前言
记得一开始接触脚手架时,就觉得这是个很神奇的东西。不用复杂的配置,不用去查文档,拿@vue/cli
为例,直接一行vue create my-project
命令,按一下回车,ok。接下来就是愉快的等待时间,看着项目快速的自动生成出来,与之前一项一项手动配置相比,效率不是快了一星半点。这简直就是解放生产力的工具。项目与项目之间有很多配置和结构都是可以复用的,而且项目的类型是有限的,可以枚举出来的,所以再搭配上相应的模板的话,就可以快速开始一个新项目了。
分析
目前采用最多并且耦合度最低的一种思路是脚手架工具与模板项目分离,即模板项目放远程仓库或者什么地方,脚手架工具将模板拉取到本地,再根据相关配置命令生成相应的项目。一句话就解释完了,好像没什么难度。这一篇主要分析脚手架工具,至于模板就是另一个话题了,下一篇《Re从零开始的路由模块编写》会讲到。接下来看看这个工具是实现的细节。
安装与使用
我们最终使用时,应该要如同@vue/cli
一样,能在命令行中全局使用。这就不得不提到npm
配置文件package.js
中的bin
字段了。
//就像这样去配置,myCli是自定义的命令名,'./bin/myCli.js'则是引导文件
{
"bin": {
"myCli": "./bin/myCli.js"
}
}
在npm install
时npm会分析这个字段
- 如果是全局安装,除了将包安装到npm安装目录下的
node_modules
外,还会在npm安装目录的bin文件下创建一个软链,链向node_modules/my-cli/bin/myCli.js
,此时在PATH
中也会加入相关的路径映射,在使用myCli create myProject
时,相当于在执行node /npm安装路径/bin/myCli create myProject
。
- 如果不是全局安装,则会在项目中的
node_modules/.bin
下创建软链,链向node_modules/my-cli/bin/myCli.js
,在npm run
时,会将相关路径映射加入到PATH
中。
这样就能做到在命令行中使用这个全局命令了
项目结构
先来看看项目结构
结构并不复杂
sc.js
就是上面介绍的引导文件cli.js
将匹配到的命令分发到不同的处理文件(cmd
目录下一堆增删改查处理程序)format.js
工具函数templates.json
保存相关配置记录
接收命令输入
这一步需要处理的功能点有几个
- 能在命令行上直接输入命令
- 需要接收来自命令行的输入
- 输入的过程是异步的,需要等待所有配置命令接收到之后,才能做相应的处理
- 处理可能发生的错误,如输入错误,网络错误等等
仔细一想,从命令行获的命令和分步处理输入的命令似乎不是那么容易完成,但问题不大。
prompts
站在巨人的肩膀上,在完成前3步之前,先介绍一个很关键的插件。prompts
这个包对命令行相关处理进行了很好的封装,配置简单,支持分步输入。
// 简单的配置即可获得相关参数
(async () => {
const preOption = [{
type: 'text',
name: 'project',
message: '项目名称?',
validate: value => value ? true : '请输入内容'
}, {
type: 'text',
name: 'url',
message: '项目仓库地址?',
validate: value => value ? true : '请输入内容'
}]
const preResponse = await prompts(preOption);
})()
// 还能设置命令提示信息
program
.version(packageInfo.version)
program
.command('init')
.description('初始化一个项目')
.alias('i')
.action(() => {
require('./cmd/init').init();
});
program
.command('add')
.description('新建一个项目')
.alias('a')
.action(() => {
require('./cmd/add').add();
});
处理相关输入
这里最核心的功能就是自动初始化项目了,除此之外还有对项目列表及配置的增删改查,毕竟我们的模板不止一个,需要维护的一个项目列表。下面看一下几段核心的代码。
初始化一个项目
(async () => {
const preOption = [{
type: 'text',
name: 'project',
message: '项目名称?',
validate: value => value ? true : '请输入内容'
}]
format.table(templates)
const preResponse = await prompts(preOption);
if (!Object.keys(preResponse).length) {
console.log(chalk.yellow('程序中断'))
process.exit();
}
const {
project
} = preResponse
if (!templates[project] || !project) {
console.log(chalk.red('模板名不为空或模板不存在'))
process.exit()
}
const {
url,
branch
} = templates[project]
spinner.start('正在初始化项目');
exec(`git clone ${url} ${project} && cd ${project} && git checkout ${branch}`, (err) => {
// 删除 git 文件
exec('cd ' + project + ' && rm -rf .git', (err, out) => {
spinner.succeed('模板拉取成功')
process.exit()
});
});
})()
增加一条项目记录
(async () => {
const option = [{
type: 'text',
name: 'project',
message: '项目名称?',
validate: value => value ? true : '请输入内容'
}, {
type: 'text',
name: 'url',
message: '项目仓库地址?',
validate: value => value ? true : '请输入内容'
}, {
type: 'text',
name: 'branch',
message: '项目分支?',
initial: 'master',
validate: value => value ? true : '请输入内容'
}, {
type: 'text',
name: 'des',
message: '项目描述?',
validate: value => value ? true : '请输入内容'
}]
const response = await prompts(option);
if (!Object.keys(response).length) {
console.log(chalk.yellow('程序中断'))
process.exit();
}
const {
project
} = response
if (templates[project] || !project) {
console.log(chalk.red('模板名不为空或模板已存在'))
process.exit()
}
templates[project] = response
fs.writeFile(__dirname + '/../../templates.json', JSON.stringify(templates), 'utf-8', (err) => {
console.log(chalk.green('添加成功'))
process.exit();
});
})();
删除一条项目记录
(async () => {
const option = [{
type: 'text',
name: 'project',
message: '项目名称?',
validate: value => value ? true : '请输入内容'
}]
const response = await prompts(option);
if (!Object.keys(response).length) {
console.log(chalk.yellow('程序中断'))
process.exit();
}
const {
project
} = response
if (!templates[project] || !project) {
console.log(chalk.red('模板名不为空或模板不存在'))
process.exit()
}
if (project in templates) {
delete templates[project]
fs.writeFile(__dirname + '/../../templates.json', JSON.stringify(templates), 'utf-8', (err) => {
console.log(chalk.green('删除成功'))
format.table(templates)
process.exit();
});
} else {
console.log(chalk.red('没有该模板'))
process.exit();
}
})()
例如像更新功能就同上面的差不多,有些细节可以查看完整代码。当然还有很多功能没有实现,这只是个简易版本,帮助理解像类似@vue/cli
这样的脚手架工具是怎么做的。
发布项目
不会吧,不会吧,2020年了居然有人不会在npm上发布项目,直接npm publish
就好了,什么居然报错?详情可看《Re从零开始的组件库构建与发布流程》
完整代码
仓库地址 https://github.com/GoldWorker/slucky-cli
结束
其实这样的工具还是很简单的,实现上借用prompts
这个包过程跳过了相对繁琐的处理命令行的部分。当然如果想要像真正的@vue/cli
那样的功能,只需要继续添加相关的命令与处理过程即可。
或者你感兴趣的内容
Re从零开始系列
- 《Re从零开始的组件库构建与发布流程》
- 《Re从零开始的UI库编写生活之规范制定》
- 《Re从零开始的UI库编写生活之按钮》
- 《Re从零开始的UI库编写生活之表单》
- 《Re从零开始的UI库编写生活之表格组件》
- 《Re从零开始的UI库编写生活-步骤管理组件Steps》
- 《Re从零开始的UI库编写生活-Tree组件》
- 《Re从零开始的后端学习之配置Ubuntu+Ngnix+Nodejs+Mysql环境》
- 《Re从零开始的后端学习之配置LAMP环境》