vue-cli 是 vue.js 官方脚手架命令行工具, 我们可以用它快速搭建 Vue.js 项目, vue-cli 最主要的功能就是初始化项目, 既可以使用官方模板, 也可以使用自定义模板生成项目, 而且从 2.8.0 版本开始, vue-cli 新增了 build 命令, 能让你零配置启动一个 Vue.js 应用接下来, 我们一起探究一下 vue-cli 是如何工作的
全局安装
首先, vue-cli 是一个 node 包, 且可以在终端直接通过 vue 命令调用, 所以 vue-cli 需要全局安装, 当 npm 全局安装一个包时, 主要做了两件事:
将包安装到全局的 node_modules 目录下
在 bin 目录下创建对应的命令, 并链接到对应的可执行脚本
看一下 vue-cli 的 package.json, 可以发现如下代码:
- {
- "bin": {
- "vue": "bin/vue",
- "vue-init": "bin/vue-init",
- "vue-list": "bin/vue-list",
- "vue-build": "bin/vue-build"
- }
- }
这样在全局安装 vue-cli 后, npm 会帮你注册 vue, vue-init, vue-list, vue-build 这几个命令
项目结构
vue-cli 项目本身也不大, 项目结构如下:
- .
- bin
- docs
- lib
- test
- e2e
bin 目录下是可执行文件, docs 下是新特性 vue build 的文档, lib 是拆分出来的类库, test 下是测试文件, 我们着重看 bin 目录下的文件即可
bin/vue
首先看 bin/vue, 内容很简短, 只有如下代码:
- #!/usr/bin/env node
- require('commander')
- .version(require('../package').version)
- .usage('<command> [options]')
- .command('init', 'generate a new project from a template')
- .command('list', 'list available official templates')
- .command('build', 'prototype a new project')
- .parse(process.argv)
vue-cli 是基于 commander.js 写的, 支持 Git-style sub-commands, 所以执行 vue init 可以达到和 vue-init 同样的效果
bin/vue-init
接下来看 bin/vue-init,vue-init 的主要作用是根据指定模板生成项目原型文件首先是引入一些依赖模块和 lib 中的辅助函数, 因为 init 命令需要接收至少一个参数, 所以 vue-init 第一个被执行到的就是检验入参的 help 函数, 如果没有传入参数, 则打印提示, 传入参数则继续运行
再向下是解析参数的过程:
- var template = program.args[0]
- var hasSlash = template.indexOf('/') > -1
- var rawName = program.args[1]
- var inPlace = !rawName || rawName === '.'
- var name = inPlace ? path.relative('../', process.cwd()) : rawName
- var to = path.resolve(rawName || '.') var clone = program.clone || false
- var tmp = path.join(home, '.vue-templates', template.replace(/\//g, '-')) if (program.offline) {
- console.log(` > Use cached template at $ {
- chalk.yellow(tildify(tmp))
- }`) template = tmp
- }
template 是模板名, 第二个参数 (program.args[1])rawName 为项目名, 如果不存在或为. 则视为在当前目录下初始化 (inPlace = true), 默认项目名称 name 也为当前文件夹名 to 是项目的输出路径, 后面会用到 clone 参数判断是否使用 git clone 的方式下载模板, 当模板在私有仓库时用得上 offline 参数决定是否使用离线模式, 如果使用离线模式, vue-cli 会尝试去~/.vue-templates 下获取对应的模板, 可以省去漫长的
downloading template
的等待时间, 但是模板是不是最新的版本就无法确定了
前面在处理参数时会得到一个变量 to, 表示即将生成的项目路径, 如果已存在, 则会输出警告, 让用户确认是否继续, 确认后执行 run 函数:
- if (exists(to)) {
- inquirer.prompt([{
- type: 'confirm',
- message: inPlace ? 'Generate project in current directory?': 'Target directory exists. Continue?',
- name: 'ok'
- }],
- function(answers) {
- if (answers.ok) {
- run()
- }
- })
- } else {
- run()
- }
run 函数主要检查了模板是否是本地模板, 然后获取或下载模板, 获取到模板后执行 generate 函数
generate 函数是生成项目的核心, 主要代码:
- module.exports = function generate (name, src, dest, done) {
- var opts = getOptions(name, src)
- // Metalsmith 读取 template 下所有资源
- var metalsmith = Metalsmith(path.join(src, 'template'))
- var data = Object.assign(metalsmith.metadata(), {
- destDirName: name,
- inPlace: dest === process.cwd(),
- noEscape: true
- })
- opts.helpers && Object.keys(opts.helpers).map(function (key) {
- Handlebars.registerHelper(key, opts.helpers[key])
- })
- var helpers = {chalk, logger}
- if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
- opts.metalsmith.before(metalsmith, opts, helpers)
- }
- // 一次使用 askQuestions, filterFiles, renderTemplateFiles 处理读取的内容
- metalsmith.use(askQuestions(opts.prompts))
- .use(filterFiles(opts.filters))
- .use(renderTemplateFiles(opts.skipInterpolation))
- if (typeof opts.metalsmith === 'function') {
- opts.metalsmith(metalsmith, opts, helpers)
- } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
- opts.metalsmith.after(metalsmith, opts, helpers)
- }
- // 将处理后的文件输出
- metalsmith.clean(false)
- .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
- .destination(dest)
- .build(function (err, files) {
- done(err)
- if (typeof opts.complete === 'function') {
- var helpers = {chalk, logger, files}
- opts.complete(data, helpers)
- } else {
- logMessage(opts.completeMessage, data)
- }
- })
- return data
- }
首先通过 getOptions 获取了一些项目的基础配置信息, 如项目名, git 用户信息等然后通过 metalsmith 结合 askQuestions,filterFiles,
renderTemplateFiles
这几个中间件完成了项目模板的生成过程 **metalsmith** 是一个插件化的静态网站生成器, 它的一切都是通过插件运作的, 这样可以很方便地为其扩展 通过 generate 函数的代码, 很容易看出来生成项目的过程主要是以下几个阶段
每个过程主要用了以下库:
getOptions: 主要是读取模板下的 meta.json 或 meta.js,meta.json 是必须的文件, 为 cli 提供多种信息, 例如自定义的 helper, 自定义选项, 文件过滤规则等等该如何写一个自定义模板, 可以参考这里
通过 Metalsmith 读取模板内容, 需要注意的是, 此时的模板内容还是未被处理的, 所以大概长这样:
- /* eslint-disable no-new */
- new Vue({
- el: '#app',
- {{#router}}
- router,
- {{/router}}
- {{#if_eq build "runtime"}}
- render: h => h(App){{#if_eq lintConfig "airbnb"}},{{/if_eq}}
- {{/if_eq}}
- {{#if_eq build "standalone"}}
- template: '<App/>',
- components: { App }{{#if_eq lintConfig "airbnb"}},{{/if_eq}}
- {{/if_eq}}
- }){{#if_eq lintConfig "airbnb"}};{{/if_eq}}
获取自定义配置: 主要是通过 async 和 inquirer 的配合完成收集用户自定义配置
filterFiles: 对文件进行过滤, 通过 minimatch 进行文件匹配
渲染模板: 通过 consolidate.js 配合 handlebars 渲染文件
输出: 直接输出
vue-init 的整个工作流程大致就是这样, vue-cli 作为一个便捷的命令行工具, 其代码写的也简洁易懂, 而且通过分析源码, 可以发现其中用到的很多有意思的模块
bin/vue-list
vue-list 功能很简单, 拉取 vuejs-templates 的模板信息并输出
bin/vue-build
vue-build 则是通过一份 webpack 配置将项目跑起来, 如果是入口仅是一个. vue 组件, 就使用默认的 default-entry.es6 加载组件并渲染
其他
在看 vue-cli 源码时, 发现了 user-home 这个模块, 这个模块的内容如下:
- 'use strict';
- module.exports = require('os-homedir')();
os-homedir 这个包是一个 os.homedir 的 polyfill, 在 Why not just use the os-home module? 下, 我看到了 Modules are cheap in Node.js 这个 blog 事实上 sindresorhus 写了很多的 One-line node modules, 他也很喜欢 One-line node moduels, 因为模块越小, 就意味着灵活性和重用性更高当然对于 One-line modules, 每个人的看法不一样, 毕竟也不是第一次听到 ** 就这一个函数也 tm 能写个包 ** 的话了我认为这个要因人而异, sindresorhus 何许人也, 很多著名开源项目的作者, 发布的 npm 包 1000+, 大多数他用到的模块, 都是他自己写的, 所以对他来说, 使用各种积木去组建高楼得心应手不过对于其他人来说, 如果习惯于这种方式, 可能会对这些东西依赖性变强, 就像现在很多前端开发依赖框架而不重基础一样, 所以我认为这种拼积木开发方式挺好, 但最好还是要知其所以然但是我感觉 One-line modules 的作用却不大, 就像 user-home 这个模块, 如果没有它,
const home = require('os-homedir')();
也可以达到目的, 可能处于强迫症的原因, user-home 才诞生了吧, 而且像 negative-zero 这样的 One-line modules, 使用场景少是其一, 而且也没带来什么方便, 尤其是 2.0 版本, 这个包直接使用 Object.is 去判断了:
- 'use strict';
- module.exports = x = >Object.is(x, -0);
不知道大家对 One-line modules 是什么看法?
来源: https://juejin.im/post/5a7b1b86f265da4e8f049081