Vue-cli 原理分析

背景

在平時工做中會有遇到許多以相同模板定製的小程序,所以想本身創建一個生成模板的腳手架工具,以模板爲基礎構建對應的小程序,而平時的小程序都是用mpvue框架來寫的,所以首先先參考一下Vue-cli的原理。知道原理以後,再定製本身的模板腳手架確定是事半功倍的。html

在說代碼以前咱們首先回顧一下Vue-cli的使用,咱們一般使用的是webpack模板包,輸入的是如下代碼。vue

vue init webpack [project-name]
在執行這段代碼以後,系統會自動下載模板包,隨後會詢問咱們一些問題,好比模板名稱,做者,是否須要使用eslint,使用npm或者yarn進行構建等等,當全部問題咱們回答以後,就開始生成腳手架項目。node

咱們將源碼下載下來,源碼倉庫點擊這裏,平時用的腳手架仍是2.0版本,要注意,默認的分支是在dev上,dev上是3.0版本。webpack

咱們首先看一下package.json,在文件當中有這麼一段話git

{
github

"bin": {web

"vue":"bin/vue",vuex

"vue-init":"bin/vue-init",vue-cli

"vue-list":"bin/vue-list"npm

}

}

因而可知,咱們使用的命令 vue init,應該是來自bin/vue-init這個文件,咱們接下來看一下這個文件中的內容

bin/vue-init

constdownload =require('download-git-repo')

constprogram =require('commander')

constexists =require('fs').existsSync

constpath =require('path')

constora =require('ora')

consthome =require('user-home')

consttildify =require('tildify')

constchalk =require('chalk')

constinquirer =require('inquirer')

constrm =require('rimraf').sync

constlogger =require('../lib/logger')

constgenerate =require('../lib/generate')

constcheckVersion =require('../lib/check-version')

constwarnings =require('../lib/warnings')

constlocalPath =require('../lib/local-path')

download-git-repo 一個用於下載git倉庫的項目的模塊
commander 能夠將文字輸出到終端當中
fs 是node的文件讀寫的模塊
path 模塊提供了一些工具函數,用於處理文件與目錄的路徑
ora 這個模塊用於在終端裏有顯示載入動畫
user-home 獲取用戶主目錄的路徑
tildify 將絕對路徑轉換爲波形路徑 好比/Users/sindresorhus/dev → ~/dev
inquirer 是一個命令行的回答的模塊,你能夠本身設定終端的問題,而後對這些回答給出相應的處理
rimraf 是一個可使用 UNIX 命令 rm -rf的模塊
剩下的本地路徑的模塊其實都是一些工具類,等用到的時候咱們再來說

// 是否爲本地路徑的方法 主要是判斷模板路徑當中是否存在 ./
const isLocalPath = localPath.isLocalPath
// 獲取模板路徑的方法 若是路徑參數是絕對路徑 則直接返回 若是是相對的 則根據當前路徑拼接

constgetTemplatePath = localPath.getTemplatePath

/**

* Usage.

*/

program

.usage('<template-name> [project-name]')

.option('-c, --clone','use git clone')

.option('--offline','use cached template')

/**

* Help.

*/

program.on('--help', () => {

console.log('  Examples:')

console.log()

console.log(chalk.gray('    # create a new project with an official template'))

console.log('    $ vue init webpack my-project')

console.log()

console.log(chalk.gray('    # create a new project straight from a github template'))

console.log('    $ vue init username/repo my-project')

console.log()

})

/**

* Help.

*/

functionhelp(){

program.parse(process.argv)

if(program.args.length <1)returnprogram.help()

}

help()

這部分代碼聲明瞭vue init用法,若是在終端當中 輸入 vue init --help或者跟在vue init 後面的參數長度小於1,也會輸出下面的描述

Usage: vue-init  [project-name]

Options:

-c, --cloneusegit clone

--offlineusecached template

-h, --help   output usage information

Examples:

# create a new project with an official template

$ vue init webpackmy-project

# create a new project straight from a github template

$ vue init username/repomy-project

接下來是一些變量的獲取

/**Settings.*/

// 模板路徑

lettemplate = program.args[0]

consthasSlash = template.indexOf('/') >-1

// 項目名稱

constrawName = program.args[1]

constinPlace = !rawName || rawName ==='.'

// 若是不存在項目名稱或項目名稱輸入的'.' 則name取的是 當前文件夾的名稱

constname = inPlace ? path.relative('../', process.cwd()) : rawName

// 輸出路徑

constto = path.resolve(rawName ||'.')

// 是否須要用到 git clone

constclone = program.clone ||false

// tmp爲本地模板路徑 若是 是離線狀態 那麼模板路徑取本地的

consttmp = path.join(home,'.vue-templates', template.replace(/[/:]/g,'-'))

if(program.offline) {

console.log(`> Use cached template at${chalk.yellow(tildify(tmp))}`)

template = tmp

}

接下來主要是根據模板名稱,來下載並生產模板,若是是本地的模板路徑,就直接生成。

/**

* Check, download and generate the project.

*/

functionrun(){

// 判斷是不是本地模板路徑

if(isLocalPath(template)) {

// 獲取模板地址

consttemplatePath = getTemplatePath(template)

// 若是本地模板路徑存在 則開始生成模板

if(exists(templatePath)) {

generate(name, templatePath, to, err => {

if(err) logger.fatal(err)

console.log()

logger.success('Generated "%s".', name)

})

}else{

logger.fatal('Local template "%s" not found.', template)

}

}else{

// 非本地模板路徑 則先檢查版本

checkVersion(()=>{

// 路徑中是否 包含'/'

// 若是沒有 則進入這個邏輯

if(!hasSlash) {

// 拼接路徑 'vuejs-tempalte'下的都是官方的模板包

constofficialTemplate ='vuejs-templates/'+ template

// 若是路徑當中存在 '#'則直接下載

if(template.indexOf('#') !==-1) {

downloadAndGenerate(officialTemplate)

}else{

// 若是不存在 -2.0的字符串 則會輸出 模板廢棄的相關提示

if(template.indexOf('-2.0') !==-1) {

warnings.v2SuffixTemplatesDeprecated(template, inPlace ?'': name)

return

}

// 下載並生產模板

downloadAndGenerate(officialTemplate)

}

}else{

// 下載並生生成模板

downloadAndGenerate(template)

}

})

}

}

咱們來看下 downloadAndGenerate這個方法

/**

* Download a generate from a template repo.

*

*@param{String} template

*/

functiondownloadAndGenerate(template){

// 執行加載動畫

constspinner = ora('downloading template')

spinner.start()

// Remove if local template exists

// 刪除本地存在的模板

if(exists(tmp)) rm(tmp)

// template參數爲目標地址 tmp爲下載地址 clone參數表明是否須要clone

download(template, tmp, {clone}, err => {

// 結束加載動畫

spinner.stop()

// 若是下載出錯 輸出日誌

if(err) logger.fatal('Failed to download repo '+ template +': '+ err.message.trim())

// 模板下載成功以後進入生產模板的方法中 這裏咱們再進一步講

generate(name, tmp, to, err => {

if(err) logger.fatal(err)

console.log()

logger.success('Generated "%s".', name)

})

})

}

到這裏爲止,bin/vue-init就講完了,該文件作的最主要的一件事情,就是根據模板名稱,來下載生成模板,可是具體下載和生成的模板的方法並不在裏面。

下載模板

下載模板用的download方法是屬於download-git-repo模塊的。

最基礎的用法爲以下用法,這裏的參數很好理解,第一個參數爲倉庫地址,第二個爲輸出地址,第三個是否須要 git clone,帶四個爲回調參數

download('flipxfx/download-git-repo-fixture','test/tmp',{clone:true},function(err){

console.log(err ?'Error':'Success')

})

在上面的run方法中有提到一個#的字符串實際就是這個模塊下載分支模塊的用法

download('bitbucket:flipxfx/download-git-repo-fixture#my-branch','test/tmp', {clone:true},function(err){

console.log(err ?'Error':'Success')

})

生成模板

模板生成generate方法在generate.js當中,咱們繼續來看一下

generate.js

constchalk =require('chalk')

constMetalsmith =require('metalsmith')

constHandlebars =require('handlebars')

constasync=require('async')

constrender =require('consolidate').handlebars.render

constpath =require('path')

constmultimatch =require('multimatch')

constgetOptions =require('./options')

constask =require('./ask')

constfilter =require('./filter')

constlogger =require('./logger')

chalk 是一個可讓終端輸出內容變色的模塊
Metalsmith是一個靜態網站(博客,項目)的生成庫
handlerbars 是一個模板編譯器,經過template和json,輸出一個html
async 異步處理模塊,有點相似讓方法變成一個線程
consolidate 模板引擎整合庫
multimatch 一個字符串數組匹配的庫
options 是一個本身定義的配置項文件

隨後註冊了2個渲染器,相似於vue中的 vif velse的條件渲染

// register handlebars helper

Handlebars.registerHelper('if_eq',function(a, b, opts){

returna === b

? opts.fn(this)

: opts.inverse(this)

})

Handlebars.registerHelper('unless_eq',function(a, b, opts){

returna === b

? opts.inverse(this)

: opts.fn(this)

})

接下來看關鍵的generate方法

module.exports =functiongenerate(name, src, dest, done){

// 讀取了src目錄下的 配置文件信息, 同時將 name auther(當前git用戶) 賦值到了 opts 當中

constopts = getOptions(name, src)

// 拼接了目錄 src/{template} 要在這個目錄下生產靜態文件

constmetalsmith = Metalsmith(path.join(src,'template'))

// 將metalsmitch中的meta 與 三個屬性合併起來 造成 data

constdata =Object.assign(metalsmith.metadata(), {

destDirName: name,

inPlace: dest === process.cwd(),

noEscape:true

})

// 遍歷 meta.js元數據中的helpers對象,註冊渲染模板數據

// 分別指定了 if_or 和   template_version內容

opts.helpers &&Object.keys(opts.helpers).map(key=>{

Handlebars.registerHelper(key, opts.helpers[key])

})

consthelpers = { chalk, logger }

// 將metalsmith metadata 數據 和 { isNotTest, isTest 合併 }

if(opts.metalsmith &&typeofopts.metalsmith.before ==='function') {

opts.metalsmith.before(metalsmith, opts, helpers)

}

// askQuestions是會在終端裏詢問一些問題

// 名稱 描述 做者 是要什麼構建 在meta.js 的opts.prompts當中

// filterFiles 是用來過濾文件

// renderTemplateFiles 是一個渲染插件

metalsmith.use(askQuestions(opts.prompts))

.use(filterFiles(opts.filters))

.use(renderTemplateFiles(opts.skipInterpolation))

if(typeofopts.metalsmith ==='function') {

opts.metalsmith(metalsmith, opts, helpers)

}elseif(opts.metalsmith &&typeofopts.metalsmith.after ==='function') {

opts.metalsmith.after(metalsmith, opts, helpers)

}

// clean方法是設置在寫入以前是否刪除原先目標目錄 默認爲true

// source方法是設置原路徑

// destination方法就是設置輸出的目錄

// build方法執行構建

metalsmith.clean(false)

.source('.')// start from template root instead of `./src` which is Metalsmith's default for `source`

.destination(dest)

.build((err, files) =>{

done(err)

if(typeofopts.complete ==='function') {

// 當生成完畢以後執行 meta.js當中的 opts.complete方法

consthelpers = { chalk, logger, files }

opts.complete(data, helpers)

}else{

logMessage(opts.completeMessage, data)

}

})

returndata

}

meta.js
接下來看如下complete方法

complete:function(data, { chalk }){

constgreen = chalk.green

// 會將已有的packagejoson 依賴聲明從新排序

sortDependencies(data, green)

constcwd = path.join(process.cwd(), data.inPlace ?'': data.destDirName)

// 是否須要自動安裝 這個在以前構建前的詢問當中 是咱們本身選擇的

if(data.autoInstall) {

// 在終端中執行 install 命令

installDependencies(cwd, data.autoInstall, green)

.then(()=>{

returnrunLintFix(cwd, data, green)

})

.then(()=>{

printMessage(data, green)

})

.catch(e=>{

console.log(chalk.red('Error:'), e)

})

}else{

printMessage(data, chalk)

}

}

構建自定義模板

在看完vue-init命令的原理以後,其實定製自定義的模板是很簡單的事情,咱們只要作2件事

首先咱們須要有一個本身模板項目
若是須要自定義一些變量,就須要在模板的meta.js當中定製
因爲下載模塊使用的是download-git-repo模塊,它自己是支持在github,gitlab,bitucket上下載的,到時候咱們只須要將定製好的模板項目放到git遠程倉庫上便可。

因爲我須要定義的是小程序的開發模板,mpvue自己也有一個quickstart的模板,那麼咱們就在它的基礎上進行定製,首先咱們將它fork下來,新建一個custom分支,在這個分支上進行定製。

咱們須要定製的地方有用到的依賴庫,須要額外用到less以及wxparse
所以咱們在 template/package.json當中進行添加

{

// ... 部分省略

"dependencies": {

"mpvue":"^1.0.11"{{#vuex}},

"vuex":"^3.0.1"{{/vuex}}

},

"devDependencies": {

// ... 省略

// 這是添加的包

"less":"^3.0.4",

"less-loader":"^4.1.0",

"mpvue-wxparse":"^0.6.5"

}

}

除此以外,咱們還須要定製一下eslint規則,因爲只用到standard,所以咱們在meta.js當中 能夠將 airbnb風格的提問刪除

"lintConfig": {

"when":"lint",

"type":"list",

"message":"Pick an ESLint preset",

"choices": [

{

"name":"Standard (https://github.com/feross/standard)",

"value":"standard",

"short":"Standard"

},

{

"name":"none (configure it yourself)",

"value":"none",

"short":"none"

}

]

}

.eslinttrc.js

'rules': {

{{#if_eq lintConfig "standard"}}

"camelcase":0,

//allow paren-less arrow functions

"arrow-parens":0,

"space-before-function-paren":0,

//allow async-await

"generator-star-spacing":0,

{{/if_eq}}

{{#if_eq lintConfig "airbnb"}}

//don't require .vue extension when importing

'

import/extensions': ['error', 'always', {

'

js': 'never',

'

vue': 'never'

}],

// allow optionalDependencies

'

import/no-extraneous-dependencies': ['error', {

'

optionalDependencies': ['test/unit/index.js']

}],

{{/if_eq}}

// allow debugger during development

'

no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0

}

最後咱們在構建時的提問當中,再設置一個小程序名稱的提問,而這個名稱會設置到導航的標題當中。
提問是在meta.js當中添加

"prompts": {

"name": {

"type":"string",

"required":true,

"message":"Project name"

},

// 新增提問

"appName": {

"type":"string",

"required":true,

"message":"App name"

}

}

main.json

{

"pages": [

"pages/index/main",

"pages/counter/main",

"pages/logs/main"

],

"window": {

"backgroundTextStyle":"light",

"navigationBarBackgroundColor":"#fff",

// 根據提問設置標題

"navigationBarTitleText":"{{appName}}",

"navigationBarTextStyle":"black"

}

}

最後咱們來嘗試一下咱們本身的模板

vue init Baifann/mpvue-quickstart#custom min-app-project

總結

以上模板的定製是十分簡單的,在實際項目上確定更爲複雜,可是按照這個思路應該都是可行的。好比說將一些自行封裝的組件也放置到項目當中等等,這裏就再也不細說。原理解析都是基於vue-cli 2.0的,但實際上 3.0也已經整裝待發,若是後續有機會,深刻了解以後,再和你們分享,謝謝你們。

https://github.com/BooheeFE/weekly/issues/9

做者:Baifann 

感興趣的小夥伴,能夠關注公衆號【grain先森】,回覆關鍵詞 「vue」,獲取更多資料,更多關鍵詞玩法期待你的探索~

本文同步分享在 博客「grain先森」(JianShu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索