{
name: 'Router',
value: 'router',
description: 'Structure the app with dynamic pages',
link: 'https://router.vuejs.org/',
},
{
name: 'historyMode',
when: answers => answers.features.includes('router'),
type: 'confirm',
message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,
link: 'https://router.vuejs.org/guide/essentials/history-mode.html',
},
第二个问题中有一个属性 when,它的值是一个函数 answers => answers.features.includes('router')。当函数的执行结果为 true,第二个问题才会显示出来。如果你在上一个问题中选择了 router,它的结果就会变为 true。弹出第二个问题:问你路由模式是否选择 history 模式。
// 向入口文件 `src/main.js` 注入代码 import store from './store'
generator.injectImports(generator.entryFile, `import store from './store'`)
// 向入口文件 `src/main.js` 的 new Vue() 注入选项 store
generator.injectRootOptions(generator.entryFile, `store`)
最近在学习 vue-cli 的源码,获益良多。为了让自己理解得更加深刻,我决定模仿它造一个轮子,争取尽可能多的实现原有的功能。
我将这个轮子分成三个版本:
有人可能不懂脚手架是什么。按我的理解,脚手架就是帮助你把项目的基础架子搭好。例如项目依赖、模板、构建工具等等。让你不用从零开始配置一个项目,尽可能快的进行业务开发。
建议在阅读本文时,能够结合项目源码一起配合使用,效果更好。这是项目地址 mini-cli。项目中的每一个分支都对应一个版本,例如第一个版本对应的 git 分支为 v1。所以在阅读源码时,记得要切换到对应的分支。
第一个版本 v1
第一个版本的功能比较简单,大致为:
package.json
文件,并添加对应的依赖项。index.html
、main.js
、App.vue
等文件)。npm install
命令安装依赖。项目目录树:
处理用户命令
脚手架第一个功能就是处理用户的命令,这需要使用 commander.js。这个库的功能就是解析用户的命令,提取出用户的输入交给脚手架。例如这段代码:
它使用 commander 注册了一个
create
命令,并设置了脚手架的版本和描述。我将这段代码保存在项目下的bin
目录,并命名为mvc.js
。然后在package.json
文件添加这段代码:再执行 npm link,就可以将
mvc
注册成全局命令。这样在电脑上的任何地方都能使用mvc
命令了。实际上,就是用mvc
命令来代替执行node ./bin/mvc.js
。假设用户在命令行上输入
mvc create demo
(实际上执行的是node ./bin/mvc.js create demo
),commander
解析到命令create
和参数demo
。然后脚手架可以在action
回调里取到参数name
(值为 demo)。和用户交互
取到用户要创建的项目名称
demo
之后,就可以弹出交互选项,询问用户要创建的项目需要哪些功能。这需要用到 Inquirer.js。Inquirer.js
的功能就是弹出一个问题和一些选项,让用户选择。并且选项可以指定是多选、单选等等。例如下面的代码:
弹出的问题和选项如下:
问题的类型
"type": "checkbox"
是checkbox
说明是多选。如果两个选项都进行选中的话,返回来的值为:其中
features
是上面问题中的name
属性。features
数组中的值则是每个选项中的value
。Inquirer.js
还可以提供具有相关性的问题,也就是上一个问题选择了指定的选项,下一个问题才会显示出来。例如下面的代码:第二个问题中有一个属性
when
,它的值是一个函数answers => answers.features.includes('router')
。当函数的执行结果为true
,第二个问题才会显示出来。如果你在上一个问题中选择了router
,它的结果就会变为true
。弹出第二个问题:问你路由模式是否选择history
模式。大致了解
Inquirer.js
后,就可以明白这一步我们要做什么了。主要就是将脚手架支持的功能配合对应的问题、可选值在控制台上展示出来,供用户选择。获取到用户具体的选项值后,再渲染模板和依赖。有哪些功能
先来看一下第一个版本支持哪些功能:
由于这是一个 vue 相关的脚手架,所以 vue 是默认提供的,不需要用户选择。另外构建工具 webpack 提供了开发环境和打包的功能,也是必需的,不用用户进行选择。所以可供用户选择的功能只有 4 个:
现在我们先来看一下这 4 个功能对应的交互提示语相关的文件。它们全部放在
lib/promptModules
目录下:每个文件包含了和它相关的所有交互式问题。例如刚才的示例,说明
router
相关的问题有两个。下面再看一下babel.js
的代码:只有一个问题,就是问下用户需不需要
babel
功能,默认为checked: true
,也就是需要。注入问题
用户使用
create
命令后,脚手架需要将所有功能的交互提示语句聚合在一起:以上代码的逻辑如下:
creator
对象getPromptModules()
获取所有功能的交互提示语PromptModuleAPI
将所有交互提示语注入到creator
对象const answers = await inquirer.prompt(creator.getFinalPrompts())
在控制台弹出交互语句,并将用户选择结果赋值给answers
变量。如果所有功能都选上,
answers
的值为:项目模板
获取用户的选项后就该开始渲染模板和生成
package.json
文件了。先来看一下如何生成package.json
文件:先定义一个
pkg
变量来表示package.json
文件,并设定一些默认值。所有的项目模板都放在
lib/generator
目录下:每个模板的功能都差不多:
pkg
变量注入依赖项注入依赖
下面是
babel
相关的代码:可以看到,模板调用
generator
对象的extendPackage()
方法向pkg
变量注入了babel
相关的所有依赖。注入依赖的过程就是遍历所有用户已选择的模板,并调用
extendPackage()
注入依赖。渲染模板
脚手架是怎么渲染模板的呢?用
vuex
举例,先看一下它的代码:可以看到渲染的代码为
generator.render('./template', {})
。./template
是模板目录的路径:所有的模板代码都放在
template
目录下,vuex
将会在用户创建的目录下的src
目录生成store
文件夹,里面有一个index.js
文件。它的内容为:这里简单描述一下
generator.render()
的渲染过程。第一步, 使用 globby 读取模板目录下的所有文件:
第二步,遍历所有读取的文件。如果文件是二进制文件,则不作处理,渲染时直接生成文件。否则读取文件内容,再调用 ejs 进行渲染:
使用
ejs
的好处,就是可以结合变量来决定是否渲染某些代码。例如webpack
的模板中有这样一段代码:ejs
可以根据用户是否选择了babel
来决定是否渲染这段代码。如果hasBabel
为false
,则这段代码:将不会被渲染出来。
hasBabel
的值是调用render()
时用参数传过去的:第三步,注入特定代码。回想一下刚才
vuex
中的:这两行代码的作用是:在项目入口文件
src/main.js
中注入特定的代码。vuex
是vue
的一个状态管理库,属于vue
全家桶中的一员。如果创建的项目没有选择vuex
和vue-router
。则src/main.js
的代码为:如果选择了
vuex
,它会注入上面所说的两行代码,现在src/main.js
代码变为:这里简单描述一下代码的注入过程:
提取
package.json
的部分选项一些第三方库的配置项可以放在
package.json
文件,也可以自己独立生成一份文件。例如babel
在package.json
中注入的配置为:我们可以调用
generator.extractConfigFiles()
将内容提取出来并生成babel.config.js
文件:生成文件
渲染好的模板文件和
package.json
文件目前还是在内存中,并没有真正的在硬盘上创建。这时可以调用writeFileTree()
将文件生成:这段代码的逻辑如下:
例如现在一个文件路径为
src/test.js
,第一次写入时,由于还没有src
目录。所以会先生成src
目录,再生成test.js
文件。webpack
webpack 需要提供开发环境下的热加载、编译等服务,还需要提供打包服务。目前 webpack 的代码比较少,功能比较简单。而且生成的项目中,webpack 配置代码是暴露出来的。这留待 v3 版本再改进。
添加新功能
添加一个新功能,需要在两个地方添加代码:分别是
lib/promptModules
和lib/generator
。在lib/promptModules
中添加的是这个功能相关的交互提示语。在lib/generator
中添加的是这个功能相关的依赖和模板代码。不过不是所有的功能都需要添加模板代码的,例如
babel
就不需要。在添加新功能时,有可能会对已有的模板代码造成影响。例如我现在需要项目支持ts
。除了添加ts
相关的依赖,还得在webpack
vue
vue-router
vuex
linter
等功能中修改原有的模板代码。举个例子,在
vue-router
中,如果支持ts
,则这段代码:需要修改为:
因为
ts
的值有类型。总之,添加的新功能越多,各个功能的模板代码也会越来越多。并且还需要考虑到各个功能之间的影响。
下载依赖
下载依赖需要使用 execa,它可以调用子进程执行命令。
调用
executeCommand()
开始下载依赖,参数为npm install
和用户创建的项目路径。为了能让用户看到下载依赖的过程,我们需要使用下面的代码将子进程的输出传给主进程,也就是输出到控制台:下面我用动图演示一下 v1 版本的创建过程:
创建成功的项目截图:
第二个版本 v2
第二个版本在 v1 的基础上添加了一些辅助功能:
覆盖和合并
创建项目时,先提前判断一下该项目是否存在:
如果选择
overwrite
,则进行移除fs.remove(targetDir)
。默认配置和手动模式
先在代码中提前把默认配置的代码写好:
这个配置默认使用
babel
和eslint
。然后生成交互提示语时,先调用
getDefaultPrompts()
方法获取默认配置。这样配置后,在用户选择功能前会先弹出这样的提示语:
包管理器
在
vue-cli
创建项目时,会生成一个.vuerc
文件,里面会记录一些关于项目的配置信息。例如使用哪个包管理器、npm 源是否使用淘宝源等等。为了避免和vue-cli
冲突,本脚手架生成的配置文件为.mvcrc
。这个
.mvcrc
文件保存在用户的home
目录下(不同操作系统目录不同)。我的是 win10 操作系统,保存目录为C:\Users\bin
。获取用户的home
目录可以通过以下代码获取:.mvcrc
文件还会保存用户创建项目的配置,这样当用户重新创建项目时,就可以直接选择以前创建过的配置,不用再一步步的选择项目功能。在第一次创建项目时,
.mvcrc
文件是不存在的。如果这时用户还安装了 yarn,脚手架就会提示用户要使用哪个包管理器:当用户选择 yarn 后,下载依赖的命令就会变为
yarn
;如果选择了 npm,下载命令则为npm install
:切换 npm 源
当用户选择了项目功能后,会先调用
shouldUseTaobao()
方法判断是否需要切换淘宝源:上面代码的逻辑为:
.mvcrc
是否有useTaobaoRegistry
选项。如果有,直接将结果返回,无需判断。get
请求,通过Promise.race()
来调用。这样更快的那个请求会先返回,从而知道是默认源还是淘宝源速度更快。await execa(command, ['config', 'set', 'registry', registries.taobao])
将当前 npm 的源改为淘宝源,即npm config set registry https://registry.npm.taobao.org
。如果是 yarn,则命令为yarn config set registry https://registry.npm.taobao.org
。一点疑问
其实
vue-cli
是没有这段代码的:这是我自己加的。主要是我没有在
vue-cli
中找到显式注册淘宝源的代码,它只是从配置文件读取出是否使用淘宝源,或者将是否使用淘宝源这个选项写入配置文件。另外 npm 的配置文件.npmrc
是可以更改默认源的,如果在.npmrc
文件直接写入淘宝的镜像地址,那 npm 就会使用淘宝源下载依赖。但 npm 肯定不会去读取.vuerc
的配置来决定是否使用淘宝源。对于这一点我没搞明白,所以在用户选择了淘宝源之后,手动调用命令注册一遍。
将项目功能保存为默认配置
如果用户创建项目时选择手动模式,在选择完一系列功能后,会弹出下面的提示语:
询问用户是否将这次的项目选择保存为默认配置,如果用户选择是,则弹出下一个提示语:
让用户输入保存配置的名称。
这两句提示语相关的代码为:
保存配置的代码为:
以上代码直接将用户的配置保存到
.mvcrc
文件中。下面是我电脑上的.mvcrc
的内容:下次再创建项目时,脚手架就会先读取这个配置文件的内容,让用户决定是否使用已有的配置来创建项目。
至此,v2 版本的内容就介绍完了。
小结
由于
vue-cli
关于插件的源码我还没有看完,所以这篇文章只讲解前两个版本的源码。v3 版本等我看完vue-cli
的源码再回来填坑,预计在 3 月初就可以完成。如果你想了解更多关于前端工程化的文章,可以看一下我写的《带你入门前端工程》。 这里是全文目录:
参考资料