背景
在平時工作中會有遇到許多以相同模板定製的小程式,因此想自己建立一個生成模板的腳手架工具,以模板為基礎構建對應的小程式,而平時的小程式都是用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').existsSync
const 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').sync
const 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 clone
const 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.render
const 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 helper
Handlebars.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
複製程式碼
總結
以上模板的定製是十分簡單的,在實際專案上肯定更為複雜,但是按照這個思路應該都是可行的。比如說將一些自行封裝的元件也放置到專案當中等等,這裡就不再細說。原理解析都是基於vue-cli 2.0的,但實際上 3.0也已經整裝待發,如果後續有機會,深入瞭解之後,再和大家分享,謝謝大家。
參考文章
-
vue-cli是如何工作的]6
廣而告之
本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。