vue-cli源码分析

vue-cli源码分析
—— 一步步实现自己的脚手架工具

基于版本2.9.3, vue-cli 3.x更改为与create-react-app类似的集成cli了,所以待有时间再研究

工作流程

根据自己的理解绘制流程图:
vue-cli流程图

第三方依赖

  • download-git-repo
    从git仓库下载项目模板
  • commander
    组织和处理命令行工具,简化命令流程
  • metalsmith
    静态站点生成工具,插件化文件处理流程,插件化类似于express的中间件
  • inquirer
    实现交互式命令行
  • handlebars
    模板渲染引擎
  • async
    异步执行管理函数
  • ora
    在终端优雅地显示进度工具
  • user-home
    Node4.x之前没有获取用户目录的api,所以这个是用来兼容之前的,其实也可以不需要,因为node版本现在都已经很高了,用require("os").homedir()代替
  • tildify
    将绝对路径转换为波浪路径,e.g. /Users/sindresorhus/dev -> ~/dev
  • chalk
    在终端打印出带有颜色的文字
  • rimraf
    实现unix command rm -rf
  • semver
    npm语义版本,可用于比较、验证版本号
  • request
    简单的http请求,用于vue list 获取所有版本
  • multimatch
    扩展自minimatch.match(),可匹配多个模式

如何让发布的npm包全局可执行

让你的npm包实现像vue-cli一样,全局执行vue命令,其实非常简单;下面简单实现一个小demo:

第一步 新建项目,初始化项目,编写的执行程序文件,存放为 项目根目录/bin/test注意文件名没有后缀

1
2
#!/usr/bin/env node
console.log('test');

第二步 添加npm bin配置

1
2
3
4
5
{
"bin": {
"test": "./bin/test"
}
}

第三步 link当前模块为全局模块

项目根目录执行以下命令

1
$ npm link

实际开发时,将项目publish到npm包时,不需要该命令,这个只是为测试,将当前模块link为全局模块后,可执行全局test命令,现在在终端执行test会打印出test,证明成功了。

现在再去看源码时,就可以直接看vue-cli bin目录下的vue文件了。

——————–分割线—————————-

补充知识

npm包实现可执行命令的探究

windows上如果要让一个文件可执行,那么这个文件应该是.exe或者.cmd后缀,或者通过程序运行,比如:node test.js;

linux上不根据扩展名来判断文件是否可执行,大家一定注意到上面的test.js文件第一行#!/usr/bin/env node,其实这个就是用来表明该文件可以在linux下执行,并且指明用node程序执行。

那么为什么在项目package.json文件加上bin属性就可以变为可执行?

如何查看可执行文件存在位置

在命令行执行npm bin -g即可查找到,比如我执行后的结果:

windows
因为在全局安装包时,npm会判断package.json是否有bin配置,如果存在,则会在npm包的环境变量设置目录下新建xxx.cmd文件和一个xxx文件(shell可执行文件,第一行为:#!/bin/sh),xxx就是在bin属性下配置的属性值,比如上面例子中写的是test文件,可执行文件名就会是test,test.cmd文件内容如下:

1
2
3
4
5
6
7
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\node_modules\test-cli\bin\test" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\node_modules\test-cli\bin\test" %*
)

%~dp0在windows中代表该bat脚本文件所在目录路径,该程序意思就是查看当前目录下是否有node程序;如果有,则直接用当前目录下的node程序执行test文件;如果不存在,直接全局调用。

linux
在linux上因为test文件本身就是可执行的,所以与window上不同的是,npm会在linux的.nvm/versions/node/v9.11.1/bin目录下新建一个软连接,指向安装目录下的bin目录下的可执行文件:

图片以pm2的软连接为例子。

介绍几个重要的第三方依赖库

1. commander

简化命令行操作,具体使用方法这里不做介绍,官网的解释已经很全面了。commander有两种使用方式:

第一种,action形式,如果匹配到对应的command就会执行对应的action

修改test文件为下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env node

const program = require('commander');

program
.command('init <dir>')
.option('-c --clone', 'download template through git-clone')
.action((dir, cmd) => {
console.log(dir);
console.log(cmd.clone);
});

program.parse(process.argv);

执行test init /temp -c,打印结果:

第二种,git-style 模式,可以将子命令分成模块

vue-cli采取的就是这种模式。现在修改test:

1
2
3
4
5
6
program
.version(require('../package.json').version)
.usage('<command> [options]')
.command('init', '生成一个新项目');

program.parse(process.argv);

增加test-init文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env node

const program = require('commander');

program
.usage('<template-name> [project-name]')
.option('-c --clone', 'use git-clone');

/**
* 确保在命令行运行 test init 无参数时显示该命令的help提示
* help指令是commander自动生成的,如果想自定义,可以查看vue-cli源码35-44行
*/

function help () {
program.parse(process.argv);
if (program.args.length < 1) return program.help();
}
help();

执行test init

可以看到执行了test-init文件,这就是commander的sub-commander形式,test文件定义了都有什么命令(注意不可以有action),当匹配到相应的命令,但是没有匹配到action时就会去找当前文件下的test-命令文件。

2. download-git-repo

支持的远程仓库:github、gitlab、bitbucket、自定义仓库

该模块整合了git-clonedownload,同时支持执行git clone命令和http协议下载两种方式。

有了这个模块我们就可以从远程下载我们提前做好的项目模板了。

使用方法:require('download-git-repo')(url, dest, option, callback)

  • url:下载路径
    格式: owner/name#my-branch e.g. vuejs-templates/webpack
    默认github仓库,如果要下在其他的,格式: gitlab:owner/name#my-branch,自定义仓库:https://mygitlab.com:owner/name#my-branch
  • dest:下载至dest目录
  • option: clone 为true时,使用git clone下载,否则使用http download
  • callback: 回调

现在,修改我们的test-init 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env node

const program = require('commander');
const path = require('path');
const gitDownload = require('download-git-repo');

program
.usage('<template-name> [project-name]')
.option('-c --clone', 'use git-clone');

/**
* 确保在命令行运行 test init 无参数时显示该命令的help提示
* help指令是commander自动生成的,如果想自定义,可以查看vue-cli源码35-44行
*/

function help () {
program.parse(process.argv);
if (program.args.length < 1) return program.help();
}
help();

let template = program.args[0];
const desc = path.join(__dirname, '../', '.vue-template/', template);

gitDownload(
template,
// 这里可以仿照vue-cli,将模板存储在硬盘根目录下,用于下次新建项目时,可以离线使用
// 我这里因为本地已经有了,所以下载在项目文件下,方便测试
desc,
{
clone: program.clone
},
err => {
if (err) {
console.log(err);
} else {
console.log('下载完成');
}
}
)

这里,因为还没又自己的模板,我们用vue-cli的模板进行测试,执行test init vuejs-templates/webpack test-vue,等待一会回发现控制台打印出下载完成。

根据以上几个步骤,如果没有模板渲染的需求,基本上就可以创作出最简版的cli工具了,但是工作中会遇到每个项目不同使用的编译环境或者依赖都不同,所以需要进一步通过命令行交互实现同一个模板用于不同项目。

补充

在看vue-cli的源码的时候,会发现vue-cli也提供了–clone的选项,但是如果我们在用vue的模板时加上–clone选项,会发现出现了报错:

主要原因:vuejs-templates/webpack的默认分支并不是master而是develop,这里需要查看download-git-repo的源码

1
2
3
4
5
6
7
8
9
10
// line 32-39
// repo.checkout默认为master,所以shallow默认为true
gitclone(url, dest, { checkout: repo.checkout, shallow: repo.checkout === 'master' }, function (err) {
if (err === undefined) {
rm(dest + '/.git')
fn()
} else {
fn(err)
}
})

进一步需要看下git-clone的源码,会发现当checkout有值时,会进行git checkout操作,并且shallow为true时,git clone 会添加–depth 1。

现在明白了,由于shallow为true,导致git clone 时只下载了最近一次commit,并且仅包含默认分支,而vuejs-templates/webpack的默认分支不是master,所以git-clone模块进行git checkout master报错。

这里,如果我们在下载自己的模板并且添加了–clone参数时需确保模板默认分支为master,当然,也可以自己写一个模块

inquirer
具体api请自行查阅官方文档,这里简单应用到我们自建的项目中

metalsmith