Vue-cli原理分析

背景

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


在說代碼之前我們首先回顧一下Vue-cli的使用,我們通常使用的是webpack模板包,輸入的是以下代碼。

vue init webpack [project-name]複製代碼

在執行這段代碼之後,系統會自動下載模板包,隨後會詢問我們一些問題,比如模板名稱,作者,是否需要使用eslint,使用npm或者yarn進行構建等等,當所有問題我們回答之後,就開始生成腳手架項目。

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

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

{ "bin": { "vue": "bin/vue", "vue-init": "bin/vue-init", "vue-list": "bin/vue-list"
 }
}
複製代碼

由此可見,我們使用的命令 vue init,應該是來自bin/vue-init這個文件,我們接下來看一下這個文件中的內容


bin/vue-init

const download = require('download-git-repo')const program = require('commander')const exists = require('fs').existsSyncconst path = require('path')const ora = require('ora')const home = require('user-home')const tildify = require('tildify')const chalk = require('chalk')const inquirer = require('inquirer')const rm = require('rimraf').syncconst logger = require('../lib/logger')const generate = require('../lib/generate')const checkVersion = require('../lib/check-version')const warnings = require('../lib/warnings')const localPath = 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// 獲取模板路徑的方法 如果路徑參數是絕對路徑 則直接返回 如果是相對的 則根據當前路徑拼接const getTemplatePath = 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.
 */function help () { program.parse(process.argv) if (program.args.length < 1) return program.help()
}help()
複製代碼

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

 Usage: vue-init <template-name> [project-name]
 Options:
 -c, --clone use git clone
 --offline use cached template
 -h, --help output usage information
 Examples: # create a new project with an official template
 $ vue init webpack my-project # create a new project straight from a github template
 $ vue init username/repo my-project
複製代碼

接下來是一些變量的獲取

/**
 * Settings.
 */// 模板路徑let template = program.args[0]const hasSlash = template.indexOf('/') > -1// 項目名稱const rawName = program.args[1]const inPlace = !rawName || rawName === '.'// 如果不存在項目名稱或項目名稱輸入的'.' 則name取的是 當前文件夾的名稱const name = inPlace ? path.relative('../', process.cwd()) : rawName// 輸出路徑const to = path.resolve(rawName || '.')// 是否需要用到 git cloneconst clone = program.clone || false// tmp爲本地模板路徑 如果 是離線狀態 那麼模板路徑取本地的const tmp = 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.
 */function run () { // 判斷是否是本地模板路徑
 if (isLocalPath(template)) { // 獲取模板地址
 const templatePath = 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'下的都是官方的模板包
 const officialTemplate = '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
 */function downloadAndGenerate (template) { // 執行加載動畫
 const spinner = 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

const chalk = require('chalk')const Metalsmith = require('metalsmith')const Handlebars = require('handlebars')const async = require('async')const render = require('consolidate').handlebars.renderconst path = require('path')const multimatch = require('multimatch')const getOptions = require('./options')const ask = require('./ask')const filter = require('./filter')const logger = require('./logger')
複製代碼

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

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

// register handlebars helperHandlebars.registerHelper('if_eq', function (a, b, opts) { return a === b
 ? opts.fn(this)
 : opts.inverse(this)
})
Handlebars.registerHelper('unless_eq', function (a, b, opts) { return a === b
 ? opts.inverse(this)
 : opts.fn(this)
})
複製代碼

接下來看關鍵的generate方法

module.exports = function generate (name, src, dest, done) { // 讀取了src目錄下的 配置文件信息, 同時將 name auther(當前git用戶) 賦值到了 opts 當中
 const opts = getOptions(name, src) // 拼接了目錄 src/{template} 要在這個目錄下生產靜態文件
 const metalsmith = Metalsmith(path.join(src, 'template')) // 將metalsmitch中的meta 與 三個屬性合併起來 形成 data
 const data = 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])
 }) const helpers = { chalk, logger } // 將metalsmith metadata 數據 和 { isNotTest, isTest 合併 }
 if (opts.metalsmith && typeof opts.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 (typeof opts.metalsmith === 'function') {
 opts.metalsmith(metalsmith, opts, helpers)
 } else if (opts.metalsmith && typeof opts.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 (typeof opts.complete === 'function') { // 當生成完畢之後執行 meta.js當中的 opts.complete方法
 const helpers = { chalk, logger, files }
 opts.complete(data, helpers)
 } else {
 logMessage(opts.completeMessage, data)
 }
 }) return data
}
複製代碼

meta.js

接下來看以下complete方法

complete: function(data, { chalk }) { const green = chalk.green // 會將已有的packagejoson 依賴聲明重新排序
 sortDependencies(data, green) const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName) // 是否需要自動安裝 這個在之前構建前的詢問當中 是我們自己選擇的
 if (data.autoInstall) { // 在終端中執行 install 命令
 installDependencies(cwd, data.autoInstall, green)
 .then(() => { return runLintFix(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複製代碼

4f19765bc13947029e2d64820859c13b


341bd3f7eae34263b5d5c85e5f2ba3b5


總結

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

e5872950c81c47bfa276fe610c8ed3a2


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章