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全局安裝一個包時,主要作了兩件事:vue
將包安裝到全局的node_modules目錄下。node
在bin目錄下建立對應的命令,並連接到對應的可執行腳本。webpack
看一下vue-cli的package.json,能夠發現以下代碼:git
{ "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
這幾個命令。es6
vue-cli項目自己也不大,項目結構以下:github
. ├── bin ├── docs ├── lib └── test └── e2e
bin
目錄下是可執行文件,docs
下是新特性vue build
的文檔,lib
是拆分出來的類庫,test
下是測試文件,咱們着重看bin
目錄下的文件便可。web
首先看bin/vue
,內容很簡短,只有以下代碼:vue-cli
#!/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
一樣的效果。npm
接下來看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}}
filterFiles: 對文件進行過濾,經過minimatch進行文件匹配。
渲染模板:經過consolidate.js配合handlebars渲染文件。
輸出:直接輸出
vue-init
的整個工做流程大體就是這樣,vue-cli
做爲一個便捷的命令行工具,其代碼寫的也簡潔易懂,並且經過分析源碼,能夠發現其中用到的不少有意思的模塊。
vue-list功能很簡單,拉取vuejs-templates的模板信息並輸出。
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是什麼見解?