vue create my-vue-project
用过Vue的同学基本都接触过vue脚手架:vue-cli,通过上面那行命令,回答几个问题,我们就能轻松创建出一个Vue项目。因为团队工程化需要,我最近开始学习cli,打算用作团队脚手架或收拢个人常用命令(如按照格式提交代码)。
vue-cli的原理网上已经有很多不错的分析,而本文将从基础知识学习开始,一步步进阶到实际应用。希望能帮助大家在学会使用工具的同时,了解背后的原理。养成“打破砂锅问到底”的学习习惯。
目录
- 命令执行过程
- npm包打造cli的原理
- commander原理简析
- vue-cli:create命令执行过程
- 总结
一,命令执行过程
当你在控制台敲下npm -v
并回车的时候,到底发生了什么?
我们先来看看npm
这个命令在哪。通过执行which npm
,(不了解which命令的同学可以参考这里:linux命令之which),可以看到,npm
命令的可执行文件在/usr/local/bin/npm
,打开文件夹一看,是个替身,右键“显示原身”(Mac用户的方法,其他用户请搜索“软链接”),就能定位到该命令在哪:/usr/local/lib/node_modules/npm/bin/npm-cli.js
,看到是js代码相信大家已经松了一口气,我们先不急着看npm-cli.js
里面的源,我们先思考一个问题:
Q1:系统是怎么找到
npm
这个这个命令的可执行文件的?
A1:了解
which
命令的同学都知道,which
命令会按照PATH
变量中的路径顺序来查找可执行文件。执行echo $PATH
可以打印出该变量内容:/usr/local/bin:/usr/bin:/bin
(例如这是我的部分内容,目录间用‘:’分隔),所以系统会先在/usr/local/bin
下面找npm
执行文件,/usr/local/bin/npm
链接到/usr/local/lib/node_modules/npm/bin/npm-cli.js
。所以调用npm
命令相当于执行npm-cli.js
。
总的来说:在控制台执行命令时,系统会先去环境路径(PATH)中找到可执行文件,然后执行该文件。
那么,有没有同学好奇:
Q2:系统又是怎么执行这些文件(例如上面的npm-cli)的呢?
A2:打开
npm-cli.js
文件,我们能看到的第一行代码就是:#!/usr/bin/env node
,这行代码到底有什么用呢?具体可参考:stackoverflow - #!/usr/bin/env到底有什么用?,大致意思是告知系统用什么解释程序来执行该文件,例如#!/usr/bin/env node
就是告知系统,npm-cli.js
要用node
来执行。因此npm -v
相当于node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v
。
$ node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v
6.4.1
复制代码
二,npm包打造cli的原理
了解完命令执行过程之后,我们就可以打造自己的cli命令了。
①先编写my-cli.js
文件:
#!/usr/bin/env node
console.log('Hello cli!');
复制代码
②在/usr/local/bin
下(或者PATH
里的任意路径下)创建软链接:
ln -s my-cli.js my-cli
复制代码
③给my-cli命令添加可执行权限:(若不添加权限,会报错bash: /usr/local/bin/my-cli: Permission denied
)
chmod 777 my-cli
复制代码
④验证效果:
$ my-cli
Hello cli!
复制代码
在上面的基础上,我们虽然能打造自己的命令,但是这个命令要想给团队使用,就需要每个人都拷贝my-cli.js
文件,创建软链接,添加可执行权限,非常繁琐。怎么将自己的命令分发出去给别人使用呢?
我们再往前探索一步,一起打造一个基于npm分发的命令。
我们在下载使用一个npm模块命令的时候,我们会这样:
npm install -g @vue/cli
vue create my-project
复制代码
全局安装vue-cli
这个npm模块之后,我们全局新增了vue
命令,这背后到底发生了什么?其实是npm install
帮我们把上面提到的②③步自动执行了(个人假设,还没有时间去看npm的源码,如有错误,欢迎指出)。既然npm
已经帮我们完成这些简单但是繁琐的脚本操作,那我们只需要按照npm
的规范来配置一下代码即可。流程比较简单,请参考:通过npm包来制作命令行工具的原理。
总结一下开发过程:
npm init
新建npm模块目录;- 开发命令(例如上面的
my-cli.js
); package.json
中添加bin
字段(bin: { "my-cli": "./my-cli.js" }
);- 发布npm;
- 全局安装即可使用
my-cli
;
三,commander原理简析
看了前面一二节,我们已经掌握开发一个团队cli的方法,剩下的内容将参考[vue-cli源码](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/bin/vue.js)
了解怎么优雅地实现一个cli。
这里直接和大家介绍几个vue-cli
里面用到的几个库:
- commander - 命令行参数解析库;
- Inquirer.js - 命令行常用交互形式集合(问答,选择...);
- chalk - 在命令行样式美化;
- ora - 命令行loader;
更多好用的cli开发库欢迎大家留言补充。
commander
几乎是开发cli必不可少的工具,基本使用方法如下:
#!/usr/bin/env node
var program = require('commander');
program
.version('0.1.0')
.option('-p, --peppers', 'Add peppers')
.option('-P, --pineapple', 'Add pineapple')
.option('-b, --bbq-sauce', 'Add bbq sauce')
.option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
.parse(process.argv);
复制代码
这里简单分析一下其原理(参考commander源码),核心流程如下:
1. 通过option定义收集命令的功能选项;
2. parse解析命令参数(有process获得);
3. 由命令参数去匹配前面收集到的功能选项,执行前面的方法(将参数传入);
核心源码如下:
/**
* Parse `argv`, settings options and invoking commands when defined.
*
* @param {Array} argv
* @return {Command} for chaining
* @api public
*/
Command.prototype.parse = function(argv) {
// implicit help
if (this.executables) this.addImplicitHelpCommand();
// store raw args
this.rawArgs = argv;
// guess name
this._name = this._name || basename(argv[1], '.js');
// github-style sub-commands with no sub-command
if (this.executables && argv.length < 3 && !this.defaultExecutable) {
// this user needs help
argv.push('--help');
}
// process argv
var parsed = this.parseOptions(this.normalize(argv.slice(2)));
var args = this.args = parsed.args;
var result = this.parseArgs(this.args, parsed.unknown);
// executable sub-commands
var name = result.args[0];
var aliasCommand = null;
// check alias of sub commands
if (name) {
aliasCommand = this.commands.filter(function(command) {
return command.alias() === name;
})[0];
}
if (this._execs[name] && typeof this._execs[name] !== 'function') {
return this.executeSubCommand(argv, args, parsed.unknown);
} else if (aliasCommand) {
// is alias of a subCommand
args[0] = aliasCommand._name;
return this.executeSubCommand(argv, args, parsed.unknown);
} else if (this.defaultExecutable) {
// use the default subcommand
args.unshift(this.defaultExecutable);
return this.executeSubCommand(argv, args, parsed.unknown);
}
return result;
};
复制代码
四,vue-cli:create命令执行过程
最后我们以vue create my-project
这个命令执行过程结尾,有兴趣的同学推荐看vue-cli的源码,看源码是一个很好的学习过程。
vue create my-project
命令执行过程:
- 【系统】系统定位到
bin/vue.js
文件,通过node bin/vue.js create my-project
来执行该文件; - 【vue.js】
bin/vue.js
利用commander
来定义命令选项create
,将create
命令匹配到create方法(lib/create.js
),执行该方法; - 【create.js】
lib/create.js
使用Inquirer.js来询问用户,进行项目配置; - 【Creator.js】根据用户配置生成
package.json
文件(基础信息,从项目配置中注入对应的开发依赖devDependencies); - 【Creator.js】执行
npm i
来安装依赖;(PS: 这里封装了常用的npm操作,可以直接拷贝到自己项目中使用) - 【Creator.js】加载
vue-cli
插件(@vue/cli-service是第一个被执行的插件); - 【Generator.js】执行所有插件(执行cli-service插件会生成项目文件结构);
- 【Creator.js】生成
README.md
文件;
上面就是create
命令的基本执行过程,如果我们想扩展create
方法,例如按照我们的定义的模板生成目录结构,可以新建一个插件(generator,可以参考cli-service),在插件里生成自定义的目录结构即可。
通过阅读源码有两个收获:
- 利用插件形式来扩展,能在保证核心主流程简洁可维护的同时最大限度地提高扩展灵活性(之前已经有所实践,在这里再次确认插件架构的重要意义);
- 插件API可以通过调用插件时以参数的形式注入。(相比全局挂载的方式有两个好处:①没有全局变量污染;②能按照插件类别来注入不同的API,达到权限管控的效果);
五,总结
至此,我们已经具备写出一个实现良好,易于扩展的团队cli的知识,剩下的事情,就在键盘上完成吧。
此外,我提倡“打破砂锅问到底”,而文章内显然没有做到,例如npm
甚至是node
的执行过程还没有去深入了解。时间永远是有限的,先有一个好的思考方向,再逐步去深入就好了。而这往往也能解决当代人的焦虑,方向和努力缺一不可,共勉。