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);