vue脚手架实现选项卡_从零一步步实现一个前端脚手架
前言
目前前端发展蒸蒸日上,工程化也越来越成熟。在这期间出现了很多优秀的框架和工具。与此同时伴随着与框架搭配使用的脚手架也呼之欲出。前端脚手架工具发展的日益强大,比如 vue-cli
, create-react-app
等等是在 vue
, react
开发搭建项目常用的脚手架。小编在看了 vue-cli3
, vue-cli2
的脚手架实现之后,心血来潮自己实现一个简易版的脚手架,下边我们一起来学习一下脚手架的实现流程。
实现思路
我认为 vue-cli3
和 vue-cli2
的实现区别有如下几点
- 就是
vue-cli3
不在从git
仓库下载模板,而是自己生成代码和创建文件和文件夹。 vue-cli3
把webpack
的配置内置了,不在暴露出来,提供用户自定义的配置文件来自定义自己的配置;而vue-cli2
则是把配置完全暴露出来,可以任意修改。
本文我们这里是基于 vue-cli2
的实现思路来一步步实现一个简单的 react
版本脚手架。下边是小编整体的实现过程1、添加自己脚手架的命令(lbs)2、使用 commander
工具为自己的 lbs
命令添加解析参数,解析参数,添加自定义命令;附上官方文档 [commander文档](https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md) 3、使用 inquirer
实现命令行和用户的交互(用户输入,选择);附上官方文档 [inquirer文档](https://www.npmjs.com/package/inquirer) 4、根据用户输入的项目名称,模板来下载,解压模板 5、修改模板里边的文件(package.json,index.html等) 6、为项目安装依赖,结束
开始撸代码
本文实现一个 lbs init[projectName]--force
命令projectName:
输入的项目名称 --force:
定义的选项(当前目录存在输入的 [projectName]
文件夹时候,是否强制覆盖)
添加脚手架命令(lbs)
创建项目这一步省略 利用 package.json
的 bin
项来指定自己定义的命令对应的可执行文件的位置,我们在 package.json
,添加如下代码
"bin":{
"lbs": "./bin/lbs.js"
}
然后创建 bin/lbs.js
文件,添加测试代码:
#!/usr/bin/env node
console.log("hello lbs-cli")
第一行是必须添加的,是指定这里用node解析这个脚本。默认找 /usr/bin
目录下,如果找不到去系统环境变量查找。然后我们在任意目录下打开 cmd
窗口,输入 lbs
命令,你会发现找不到命令。其实我们还需要做一步操作,就是把本地项目全局安装一下,在当前项目下执行 npm install.-g
,然后在 cmd
下执行 lbs
命令,你会发现会输出我们打印的字符串。到这里我们已经成功在系统里添加了自己定义的 lbs
命令,那么我们怎么为lbs添加init,create,--version等等参数呢?
使用commander丰富我们的lbs命令
不熟悉commander的使用请看[commander文档](https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md)我们首先要安装一下插件,然后初步尝试一下为我们的 lbs
命令添加版本查看的选项
const { program } = require("commander")
const pkg = require("./../package.json")
program.version(pkg.version,'-v --version')
program.parse(process.argv)
此时我们在任意命令行执行 lbs-v
或者 lbs--version
,可以看到在控制台输出版本信息接下来为 lbs
命令添加一个命令:
// projectName 是一个可选参数
program.command('init [projectName]')
.description("初始化项目")
// 添加一个选项
.option('-f --force','如果存在输入的项目目录,强制删除项目目录')
.action((projectName,cmd)=>{
// projectName 是我们输入的参数,
console.log(projectName)
// cmd是Command对象
console.log(cmd.force)
})
这里我们添加了一个init命令,支持一个可选参数和一个-f的可选选项 这时候我们执行一下
lbs init test -f
可以在控制台查看到我们输入的 test
和 cmd
对象。可以在cmd中查找到存在force属性。如果执行 lbs init
,输出如下如果执行 lbs init test
,输出如下这里我们主要是获取这两个数据,如果你的命令还有其它的复杂功能,还可以扩展其它参数和选项。这里只是 command
的一种使用方式,当我们为 command
添加第二个描述参数,就意味着使用独立的可执行文件作为子命令,比如你的命令是 init
那么你就需要创建一个 lbs-init
脚本文件,这个文件负责执行你指定的命令,按照 lbs-${command}
的方式创建脚本,我们创建 lbs-init.js
文件把命令修改如下,为 command
方法添加第二个参数
// projectName 是一个可选参数
program.command('init [projectName]','init project')
.description("初始化项目")
// 添加一个选项
.option('-f --force','如果存在输入的项目目录,强制删除项目目录')
.action((projectName,cmd)=>{
// projectName 是我们输入的参数,
console.log(projectName)
// cmd是Command对象
console.log(cmd.force)
})
执行 lbs init
,你会发现什么也没输出。因为这里不会执行到action方法,会去执行我们创建的 lbs-init.js
这个空文件。所以什么也不会输出。这时候 lbs.js
只需要定义 init
命令就可以了。只需要这一行就足够了 program.command('init [projectName]','init project')
然后在 lbs-init.js
添加解析代码
const { program } = require("commander")
let projectName;
let force;
// 指定解析的参数
program.arguments('[projectName]')
.description("初始化项目")
.option('-f --force','如果存在输入的项目目录,强制删除项目目录')
.action((name,cmd)=>{
projectName = name;
force = cmd.force;
});
program.parse(process.argv);
console.log(projectName,force)
重新执行 lbs init test-f
发现数据都能获取。到这里我们已经可以为我们的 lbs init
命令自定义参数和选项了,那么当用户只执行 lbs init
命令,这时候我们就获取不到项目名称,我们怎么办呢?请往下看
使用inquirer实现命令行和用户的交互(用户输入,选择,问答)
这里我们需要安装 chalk
, inquirer
插件 chalk:
主要是自定义颜色控制台输出创建一个logger.js工具类,主要是输出控制台信息
const chalk = require('chalk');
exports.warn = function(message){
console.log(chalk.yellow(message));
}
exports.error = function(message){
console.log(chalk.red(message))
}
exports.info = function(message){
console.log(chalk.white(message))
}
exports.infoGreen = function(message){
console.log(chalk.green(message))
}
exports.exit = function(error){
if(error && error instanceof Error){
console.log(chalk.red(error.message))
}
process.exit(-1);
}
这个库是我们可以和用户交互的工具;第一个问题是输入项目名称,第二个问题是让用户选择一个模板,这里的模板需要在github上准备好,我这里只准备了一个[lb-react-apps-template](https://github.com/liuboshuo/lb-react-apps-template),这个模板是基于[react-apps-template](https://github.com/liuboshuo/react-apps-template)这个项目重新建了一个git仓库。这个模板的具体实现可以可以看之前webpack的系列文章:[react+webpack4搭建前端项目](https://www.jianshu.com/p/04e436cf75ba),后边两个模板是不存在的
// 设置用户交互的问题
const questions = [
{
type: 'input',
name:'projectName',
message: chalk.yellow("输入你的项目名字:")
},
{
type:'list',
name:'template',
message: chalk.yellow("请选择创建项目模板:"),
choices:[
{
name:"lb-react-apps-template",
value:"lb-react-apps-template"
},
{name:"template2",value:"tempalte2"},
{name:"template3",value:"tempalte3"}
]
}
];
// 如果用户命令参数带projectName,只需要询问用户选择模板
if(projectName){
questions.splice(0,1);
}
// 执行用户交互命令
inquirer.prompt(questions).then(result=>{
if(result.projectName) {
projectName = result.projectName;
}
const templateName = result.template;
// 获取projectName templateName
console.log("项目名称:" + projectName)
console.log("模板名称:" + templateName)
if(!templateName || !projectName){
// 退出
logger.exit();
}
// 往下走
checkProjectExits(projectName,templateName); // 检查目录是否存在
}).catch(error=>{
logger.exit(error);
})
这里的 checkProjectExits
下边会实现,可以先忽略。这时候我们执行 lbs init
,可以看到成功获取到 projectName
和 templateName
接下来我们还需要判断用户输入的项目名称在当前目录是不是存在,在存在的情况下 1、如果用户执行的命令包含 --force
,那么直接把存在的目录删除, 2、如果命令不包含 --force
,那么需要询问用户是否需要覆盖。如果用户需要覆盖,那就直接删除存在的文件夹,不过用户不允许,那就直接退出添加 checkProjectExits
检查目录存在的方法,代码如下
function checkProjectExits(projectName,templateName){
const currentPath = process.cwd();
// 获取项目的真实路径
const filePath = path.join(currentPath,`${projectName}`);
if(force){ // 强制删除
if(fs.existsSync(filePath)){
// 删除文件夹
spinner.logWithSpinner(`删除${projectName}...`)
deletePath(filePath)
spinner.stopSpinner(false);
}
// 开始下载模板
startDownloadTemplate(projectName, templateName)
return;
}
// 判断文件是否存在 询问是否继续
if(fs.existsSync(filePath)){
inquirer.prompt( {
type: 'confirm',
name: 'out',
message: `${projectName}文件夹已存在,是否覆盖?`
}).then(data=>{
// 用户不同意
if(!data.out){
exit();
}else{
// 删除文件夹
spinner.logWithSpinner(`删除${projectName}...`)
deletePath(filePath)
spinner.stopSpinner(false);
// 开始下载模板
startDownloadTemplate(projectName, templateName)
}
}).catch(error=>{
exit(error);
})
}else{
// 开始下载模板
startDownloadTemplate(projectName, templateName)
}
}
function startDownloadTemplate(projectName,templateName){
console.log(projectName,templateName)
}
我们这里用到了一个 spinner
的工具类,新建 lib/spinner.js
,主要是一个转菊花的动画提示,代码如下
const ora = require('ora')
const chalk = require('chalk')
const spinner = ora()
let lastMsg = null
exports.logWithSpinner = (symbol, msg) => {
if (!msg) {
msg = symbol
symbol = chalk.green('✔')
}
if (lastMsg) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
}
spinner.text = ' ' + msg
lastMsg = {
symbol: symbol + ' ',
text: msg
}
spinner.start()
}
exports.stopSpinner = (persist) => {
if (!spinner.isSpinning) {
return
}
if (lastMsg && persist !== false) {
spinner.stopAndPersist({
symbol: lastMsg.symbol,
text: lastMsg.text
})
} else {
spinner.stop()
}
lastMsg = null
}
我们新建 lib/io.js
,实现 deletePath
删除目录方法,如下
function deletePath (filePath){
if(fs.existsSync(filePath)){
const files = fs.readdirSync(filePath);
for(let index=0; index
const fileNmae = files[index];
const currentPath = path.join(filePath,fileNmae);
if(fs.statSync(currentPath).isDirectory()){
deletePath(currentPath)
}else{
fs.unlinkSync(currentPath);
}
}
fs.rmdirSync(filePath);
}
}
可以创建 my-app
文件夹,这时候可以测试一下 lbs initmy-app-f
和 lbs init-f
命令,查看my-app是否删除,执行 lbs init
,根据一步步提示,输入已经存在的目录名称作为项目名称;选择模板,检查是否my-app文件夹被删除,如下
下载,解压模板
下载模板,需要我们根据选择的模板名称拼接github仓库相对应的zip压缩包的url,然后执行node的下载代码,(注意这里是把下载的zip压缩包下载到系统的临时目录)下载成功后把zip压缩包解压到用户输入项目名称的目录,解压成功后删除已下载的压缩包。这一个流程就结束了这其中下载利用 request
插件,解压用到了 decompress
插件,这两个插件需要提前安装一下,这两个插件有不熟悉使用的小伙伴可以提前熟悉一下相关使用重写上边的 startDownloadTemplate
方法
function startDownloadTemplate(projectName,templateName){
// 开始下载模板
downloadTemplate(templateName, projectName , (error)=>{
if(error){
logger.exit(error);
return;
}
// 替换解压后的模板package.json, index.html关键内容
replaceFileContent(projectName,templateName)
})
}
function replaceFileContent(projectName,templateName){
console.log(projectName,templateName);
}
新建 lib/download.js
,实现 downloadTemplate
下载模板的方法,代码如下
const request = require("request")
const fs = require("fs")
const path = require("path")
const currentPath = process.cwd();
const spinner = require("./spinner")
const os = require("os")
const { deletePath , unzipFile } = require("./io")
exports.downloadTemplate = function (templateName,projectName,callBack){
// 根据templateName拼接github对应的压缩包url
const url = `https://github.com/liuboshuo/${templateName}/archive/master.zip`;
// 压缩包下载的目录,这里是在系统临时文件目录创建一个目录
const tempProjectPath = fs.mkdtempSync(path.join(os.tmpdir(), `${projectName}-`));
// 压缩包保存的路径
const file = path.join(tempProjectPath,`${templateName}.zip`);
// 判断压缩包在系统中是否存在
if(fs.existsSync(file)){
// 删除本地系统已存在的压缩包
fs.unlinkSync(file);
}
spinner.logWithSpinner("下载模板中...")
let stream = fs.createWriteStream(file);
request(url,).pipe(stream).on("close",function(err){
spinner.stopSpinner(false)
if(err){
callBack(err);
return;
}
// 获取解压的目录
const destPath = path.join(currentPath,`${projectName}`);
// 解压已下载的模板压缩包
unzipFile(file,destPath,(error)=>{
// 删除创建的临时文件夹
deletePath(tempProjectPath);
callBack(error);
});
})
}
在 lib/io.js
添加解压zip压缩包的方法,代码如下
const decompress = require("decompress");
exports.unzipFile = function(file,destPath,callBack){
decompress(file,destPath,{
map: file => {
// 这里可以修改文件的解压位置,
// 例如压缩包中文件的路径是 ${destPath}/lb-react-apps-template/src/index.js =》 ${destPath}/src/index.js
const outPath = file.path.substr(file.path.indexOf('/') + 1)
file.path = outPath
return file
}}
).then(files => {
callBack()
}).catch(error=>{
callBack(error)
})
}
这里可以执行以下 lbs initmy-app
测试一下
修改项目中的模板文件(package.json,index.html等)
重写 replaceFileContent
方法,这一步是把模板中的一些文件的内容修改以下,比如package.json的name,index.html的title值
function replaceFileContent(projectName,templateName){
const currentPath = process.cwd();
try{
// 读取项目的package.json
const pkgPath = path.join(currentPath,`${projectName}/package.json`);
// 读取内容
const pkg = require(pkgPath);
// 修改package.json的name属性为项目名称
pkg.name = projectName;
fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2));
const indexPath = path.join(currentPath, `${projectName}/index.html`);
let html = fs.readFileSync(indexPath).toString();
// 修改模板title为项目名称
html = html.replace(/(.*)/g,`${projectName}`)
fs.writeFileSync(indexPath,html);
}catch(error){
exit(error)
}
// 安装依赖
install(projectName)
}
function install(projectName){
console.log(projectName)
}
安装依赖
重写 install
方法,这里利用 child_process
包创建一个 node
的子进程来执行 npm install
任务。注意这里要执行的命令 npm
在不同系统有区别,在 window
下执行的是 npm.cmd
命令,在 linux
和 mac
执行的是 npm
命令 有不熟悉 child_process
使用的小伙伴可以深入学习一下,这是nodejs自带的一个包,非常有用,这里贴一下文档地址 child_process官方文档,这里利用 spawn
方法执行系统命令,还可以使用 execFileSync
方法来执行文件等等
const currentPath = process.cwd();
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
// 创建一个子进程执行npm install 任务
const nodeJob = child_process.spawn(npm , ['install'], {
stdio: 'inherit', // 指定父子进程通信方式
cwd: path.join(currentPath,projectName)
});
// 监听任务结束,提示用户创建成功,接下来的操作
nodeJob.on("close",()=>{
logger.info(`创建成功! ${projectName} 项目位于 ${path.join(currentPath,projectName)}`)
logger.info('')
logger.info('你可以执行以下命令运行开发环境')
logger.infoGreen(` cd ${projectName} `);
logger.infoGreen(` npm run dev `);
})
执行 lbs init
测试一下那么到这里一个简易版的脚手架已经完成!有什么疑问可以关注公众号私信哦~
vue脚手架实现选项卡_从零一步步实现一个前端脚手架相关推荐
- 从0搭建vue3组件库: 如何完整搭建一个前端脚手架?
脚手架是为了保证各施工过程顺利进行而搭设的工作平台.按搭设的位置分为外脚手架.里脚手架:按材料不同可分为木脚手架.竹脚手架.钢管脚手架:按构造形式分为立杆式脚手架.桥式脚手架.门式脚手架.悬吊式脚手架 ...
- 前端工程师: 我用gup4.0搭建一个前端脚手架
冬至时节, 程序员加油! 本文将会介绍如何使用gulp4来搭建项目脚手架,如果您还在使用gulp3或更老的版本,您也以通过本文的一些思想将之前的项目进行完善,更新.如果gulp不是你们团队的重点,也可 ...
- 手写一个合格的前端脚手架
为什么我们需要一套脚手架 为什么我们需要一套脚手架,它能帮助我们解决哪些痛点问题. •前端项目配置越来越繁琐.耗时,重复无意义的工作•项目结构不统一.不规范•前端项目类型繁多,不同项目不同配置,管理成 ...
- 创建一个 dva 脚手架工程
2019独角兽企业重金招聘Python工程师标准>>> 1.2 dva 安装 使用 dva-cli 命令行工具安装 dva.(本文假设已掌握 npm 基础知识) 安装 dva-cli ...
- 2021-10-27 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在
这里写自定义目录标题 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在 1 ...
- element中根据条件判断按钮是否禁用_从零动手封装一个通用的vue按钮组件
我们在使用目前最主流的前端框架vue在开发过程中,组件是一个非常重要的组成部分,可以这么说,所有的vue应用,都是由一个一个的小组件拼装而成的. 正是由于vue组件如此重要,所以vue的生态中,也非常 ...
- springboot项目结构_从零搭建Spring Boot脚手架(1):开篇以及技术选型
1. 前言 目前Spring Boot已经成为主流的Java Web开发框架,熟练掌握Spring Boot并能够根据业务来定制Spring Boot成为一个Java开发者的必备技巧,但是总是零零碎碎 ...
- java webpack web项目_零基础如何学习web前端,入门教程分享
前端作为互联网时代直接触达用户的窗口,大到我们每天浏览到的网站,小到一次点击按钮的页面,前端无处不在.并且在产品的众多开发环节之中,最能让用户直观感受到的就是前端开发.因而前端行业的广阔发展前景也吸引 ...
- vue 源码详解(零):Vue 源码流程图
vue 源码详解(零):Vue 源码流程图 最近在研究 Vue 的源码, 整理博客, 结果想到的.看到的内容实在是太多了, 不知道从何写起, 故整理了一个大致的流程图,根据这个顺序进行一一整理. 为了 ...
- 【vue案例】vue实现tab选项卡
vue实现tab选项卡 文章目录 vue实现tab选项卡 一.效果图展示 二.静态页面结构 css html javascript 三.vue实现 1.将静态结构和样式重构为基于vue模板语法的形式 ...
最新文章
- 我在犹豫是不是该收集这几首MP3
- malloc一次性最大能申请多大内存空间
- python学习------文件处理
- 算法总结 -- 博弈论(PN图)
- PHP 常用数据库操作
- 【每日SQL打卡】​​​​​​​​​​​​​​​DAY 3丨删除重复的电子邮箱【难度简单】
- 2021中国企服企业规模化获客体系建设指南
- 剖析Caffe源码之Net---NetParameter参数
- centos7 下修改网络配置
- 上有天最高,自然较为小
- easyui-textbox锁定按钮不锁定_EU5几乎锁定年度销量冠军,为何北汽新能源却高兴不起来?...
- Blackman 窗函数
- [PKKS19] 《Revealing Scenes by Inverting Structure from Motion Reconstructions》(CVPR2019)阅读笔记(完)
- pda通用扫描app_手持终端PDA盘点机盘点软件盘点APP
- 苹果手机html5定位,苹果手机常去地点可以记录多长时间?
- html5分镜头脚本范例,分镜头脚本范例
- 带你使用JS-SDK自定义微信分享效果
- GraphicsLab Project之基于物理的着色系统(Physical based shading)-直接光照
- stack around xxx is corrupted
- mysql快速导出数据(带列名)
热门文章
- 51单片机课程设计:基于TCS230/3200的颜色复制显示器
- 给Metasploit安装无Lorcon2线支持模块
- 递归的Fibonacci在数羊
- springAOP 之 前置输出
- The type XXX is not API (restriction on required library 'D:\jdk-64\jre\lib\rt.jar')
- spring3: 表达式5.2 SpEL基础
- shell 命令管理tomcat
- Mac 上Dock中添加“最近打开过的项目”(Recent Applications)
- 【转】Nginx双机热备高可用解决方案【二】
- 第26周维生素市场最新动态