手摸手從淺析vue-cli原理到定製前端開發模板

前言

從迷糊小生到職場菜鳥,遇到無數難以解決的問題。幸得職場路上有大佬手摸手悉心教導,終於學會獨自開着手扶拖拉機出混跡江湖。遙想當時初入江湖,靈魂三問「據說你vue用得挺熟練,寫個vue-cli吧?」、「這邊須要定製化前端開發模板,你來加優化方案吧?」、「把vue-cli和webpack合在一塊兒作個模板吧?」 。下面將手摸手從淺析vue-cli源碼到webpack定製開發模板,文章內容比較多,因此分紅了兩部分。css

1 背景

vue-cli是vue官方提供快速建立項目的輔助腳手架工具,能夠幫助咱們快速建立新項目。官方提供幾個經常使用的模板。具體vue-cli怎麼使用,相信大夥已經玩得賊溜,不懂的夥伴能夠查閱官方文檔。可是在平常開發中,咱們須要根據腳手架增添各類不一樣的結構和經常使用工具,單純的簡單的模板已經知足不了咱們的需求,因此想要定製化模板。「工欲善其事,必先利其器」,要想更好去定製模板,要先更瞭解vue-cli的原理,而後去定製前端模板。若有不妥之處,請輕噴,多多指教。前端

2 模板meta.js範例

單純閱讀vue-cli源碼,在生成模板處理中須要對照着meta配置文件閱讀比較清晰,這裏貼了一份本身根據webpack模板配置的有關內容vue

依賴項

sortDependencies爲項目模板的依賴排序,installDependencies是爲模板渲染完畢以後安裝項目依賴npm包,runLintFix是執行eslint格式化不合法的代碼格式node


const {
sortDependencies,
installDependencies,
runLintFix,
printMessage,
} = require('./utils')
const path = require('path')

const { addTestAnswers } = require('./scenarios')
複製代碼

配置文件選項

addTestAnswers是在模板運行測試時,將爲所選場景添加答案;helpers是模板文件預約義的模板渲染輔助函數;prompts是定義須要收集的依賴變量;filters是預約義的根據條件過濾的文件;complete是模板渲染完畢以後,執行的函數,這裏的做用是根據用戶選擇是否須要自動安裝npm包依賴和格式化代碼風格。react

輔助函數和爲選項添加答案


module.exports = {
metalsmith: {
before: addTestAnswers
},
helpers: {
if_or(v1, v2, options) {
if (v1 || v2) {
return options.fn(this);
}
return options.inverse(this);
}
},

複製代碼

定義的prompts問題

prompts: {
name: {
when: "isNotTest",
type: "string",
required: true,
message: "項目名稱"
},
router: {
when: "isNotTest",
type: "confirm",
message: "安裝vue-router?"
},
vuex: {
when: "isNotTest",
type: 'confirm',
message: '安裝vuex?'
},
autoInstall: {
when: 'isNotTest',
type: 'list',
message:
'建立項目後,咱們應該爲您運行`npm install`嗎?',
choices: [
{
name: '是的,使用npm',
value: 'npm',
short: 'npm',
},
{
name: '是的,使用yarn',
value: 'yarn',
short: 'yarn',
},
{
name: '不用了,我手動安裝它',
value: false,
short: 'no',
},
]
}
},
複製代碼

filters和complete

filters定義了根據收集的變量是否爲真來決定是否須要刪除這些文件。complete是項目構建完畢以後,執行的鉤子函數,根據腳手架vue-cli,若是complete不存在,能夠另外定義一個completeMessage字符串。webpack

filters: {
'src/router/**/*': "router",
'src/store/**/*': "vuex"
},
complete: function(data, { chalk }) {
const green = chalk.green

sortDependencies(data, green)

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

if (data.autoInstall) {
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)
}
}
};
複製代碼

定製項目內容

官方的webpack項目內容定義在webpack/template中,主要對package.jsonrouter以及readme.MD嵌入了handlerbars的語法進行條件渲染。對官方模板進行了刪減,只定義部分變量,方便理解。ios

package.json

下面{{}}是佔位,根據vue-cli腳手架收集變量值和依賴庫來條件渲染來條件渲染啊內容git

{
"name": "{{name}}",
"version": "{{version}}",
"description": "{{description}}",
"main": "./src/main.js",
"dependencies": {
{{#vuex}}
"vuex": "^3.1.0",
{{/vuex}}
{{#router}}
"vue-router": "^3.0.1",
{{/router}}
"vue": "^2.5.2"
}
// ... 省略部份內容
}
複製代碼

main.js

項目入口文件也是根據收集的庫依賴來動態增長或者刪除內容達到定製項目github

import Vue from 'vue'
import App from './App'
// {{#router}}
import router from './router'
// {{/router}}

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
el: '#app',
// {{#router}}
router,
// {{/router}}
render: h => h(App)
})
複製代碼

3 vue-cli目錄設計

本文采用的是vue-cli的版本是 2.96,如今vue-cli已經更新到了4版本,可是內部原理仍是同樣,接下來先手摸手解析vue-cli的代碼。web

大致思路

vue-cli主要提供vue init、 vue list 兩個主要命令; vue init 功能是根據提供的模板生成項目,而vue list 是羅列出官方提供可以使用的模板。官方把腳手架和模板分離,我的認爲是方便模板更新與腳手架解耦,另外一方面也爲提供第三方模板提供條件(膜拜大佬操做)。

第一次使用 vue init [模板名] [項目名]從遠程安裝模板時,會根據模板名稱是否帶有‘/’區分是第三方模板仍是官方模板進行下載,將模板下載並存入計算機用戶根目錄。下次下載時可直接從緩存中讀取模板,生成最終的開發目錄結構。

目錄結構

vue-cli主要命令有vue initvue list兩個命令,第一個命令是咱們此次主講的一個命令,vue-list是經過請求github respo的api獲取可以使用的模板列表,比較簡單。

+---bin
| |--- vue
| |--- vue-build
| |--- vue-create
| |--- vue-init
| \--- vue-list
|
|
\---lib
|--- ask.js
|--- check-version.js
|--- eval.js
|--- filter.js
|--- generate.js
|--- git-user.js
|--- local-path.js
|--- logger.js
|--- options.js
\--- warnings.js
複製代碼

bin目錄中定義了vue 、init、create、build、list,其中vue-create已經在vue-cli3以上版本使用,vue-build已移除。因此會重點去了解init和list。其中lib目錄是輔助函數目錄,會在下面詳細描述。

4 vue命令源碼

終於進入正題,這裏會按照命令vuevue listvue init順序遞進,來手摸手,手把手,衝 ─=≡Σ(((つ•̀ω•́)つ

vue命令源碼定義在bin/vue文件中,源碼中引入了commander包,commander是node.js 命令行接口的完整解決方案,這裏是定義bash的命令,具體怎麼使用請跳移tj大神commander倉庫。裏面文檔比較齊全。

#!/usr/bin/env node
const program = require('commander')
// 定義了init list build create幾個命令和版本號
program
.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')
.command('create', '(for v3 warning only)')
program.parse(process.argv)
複製代碼

當在bash中只輸入vue指令時就會輸出command定義的命令提示信息反饋。

5 vue list源碼

vue list命令是經過請求Github的api獲取當前組織倉庫列表信息,展現到bash,這個命令用到幾個常見的庫:

request: 發起處理網絡請求的庫,這裏用作請求github api
chalk: bash輸出文字高亮美觀
logger: 自定義函數,打印信息

代碼解析

#!/usr/bin/env node

const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')

// 定義用戶退出時事件
console.log()
process.on('exit', () => {
console.log()
})


// 請求vue的官方倉庫,這裏能夠看到是 https://api.github.com/users/vuejs-templates/repos
request({
url: 'https://api.github.com/users/vuejs-templates/repos',
headers: {
'User-Agent': 'vue-cli'
}
}, (err, res, body) => {
if (err) logger.fatal(err)
// 返回官方模板列表數據
const requestBody = JSON.parse(body)
if (Array.isArray(requestBody)) {
console.log(' Available official templates:')
console.log()
requestBody.forEach(repo => {
console.log(
' ' + chalk.yellow('★') +
' ' + chalk.blue(repo.name) +
' - ' + repo.description)
})
} else {
console.error(requestBody.message)
}
})
複製代碼

執行結果

在輸入vue list時,會執行上面的代碼,請求https://api.github.com/users/vuejs-templates/repos獲取到模板列表信息,以下面所示:

$ vue list

Available official templates:

★ browserify - A full-featured Browserify + vueify setup with hot- reload, linting & unit testing.
★ browserify-simple - A simple Browserify + vueify setup for quick prototyping.
★ pwa - PWA template for vue-cli based on the webpack template
★ simple - The simplest possible Vue setup in a single HTML file
★ webpack - A full-featured Webpack + vue-loader setup with hot re load, linting, testing & css extraction.
★ webpack-simple - A simple Webpack + vue-loader setup for quick p rototyping.
複製代碼

自定義倉庫信息

在自定義模板倉庫時,若是不想經過請求獲取倉庫列表信息,能夠經過在腳手架中定義一份關於倉庫信息,每次輸入vue list,讀取本地文件進行展現,不過這樣每次有模板更新,都要更新腳手架,因此爲了解耦,建議經過網絡請求讀取的方式。若是內部團隊使用,而且不平常更新能夠採用下面的方式。

經過讀取本地文件

#!/usr/bin/env node
const chalk = require("chalk");
const templates = require("../templates")
console.log(chalk.cyan('\nThere are some avaiable templates: \n'))
templates.forEach((item,index) => {
console.log(chalk.yellow('★') + ` ${chalk.green(item.name)} - ${chalk.cyan(item.descriptions)}\n`)
console.log(` url: ${chalk.underline(item.url)} \n`)
})
複製代碼

templates定義

下面是做者定製了兩個模板,一個是React + typescriptvue的模板,能夠經過vue init ly-templates/vue(或者react) 項目名進行加載。

const templates = [
{
name: "vue",
url: "https://github.com/ly-templates/vue",
descriptions: "a single page web project template for vue"
},
{
name: "react",
url: "https://github.com/ly-templates/react",
descriptions: "a single page web project template for react"
}
];
module.exports = templates
複製代碼

6 vue init源碼

終於到主菜了,通常最後都是難啃的骨頭,也是最想去了解的地方,先緩解一下緊張的神經:

vue init依賴庫

運行vue init命令首先執行vue-init文件,去撥開她神祕的..(Σσ(・Д・;)我我我只是揭開了面紗!!!)

下面會是依賴模塊的做用解析:

// 下載github倉庫的庫,支持github、gitlab
const download = require('download-git-repo')
// 定義bash命令
const program = require('commander')
// nodejs內置模塊fs判斷文件目錄是否存在函數
const exists = require('fs').existsSync
// nodejs內置模塊,處理統一路徑
const path = require('path')
// 提供動態loading視覺,生動形象
const ora = require('ora')
// 獲取系統用戶根目錄路徑
const home = require('user-home')
//將絕對路徑轉換成帶波浪符的路徑
const tildify = require('tildify')
// 將bash打印信息高亮
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')
// 判斷是否本地路徑
const isLocalPath = localPath.isLocalPath
// 獲取本地模板路徑
const getTemplatePath = localPath.getTemplatePath
複製代碼

定義help命令

vue inti 命令提供經過git clone下載github倉庫的方式,只要指定--clone或者-c的命令參數,也能夠明確指定採用用戶根目錄的緩存模板生成項目.

// 指定下載倉庫的方式和生成項目是否從緩存中讀取
program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
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()
})
// 若是隻指定了vue init,沒有指定模板名,調用help命令打印提示信息
function help () {
program.parse(process.argv)
if (program.args.length < 1) return program.help()
}
help()

複製代碼

模板指定和項目名

經過vue init webpack demo命令生成項目目錄結構,首先會先獲取模板名和項目名稱。若是當前沒有指定項目名或者指定爲'.',則會提示在當前目錄生成項目和取消選擇。

// 獲取模板名稱
let template = program.args[0]
// 判斷是官方倉庫仍是三方倉庫,好比本身定義的React模板,指定爲
'ly-templates/react'
const hasSlash = template.indexOf('/') > -1
// 獲取項目名稱
const rawName = program.args[1]
// 是否指定項目名稱,不存在或者指定爲當前目錄
const inPlace = !rawName || rawName === '.'
// 若是沒有指定項目名稱或者爲'.',則以當前的目錄的名稱爲目錄名,不然以指定的目錄名在當前目錄生成項目
const name = inPlace ? path.relative('../', process.cwd()) : rawName
// 獲取要生成目錄的絕對路徑
const to = path.resolve(rawName || '.')
// 是夠是採用git clone方式下載
const clone = program.clone || false
// 設置緩存模板目錄到用戶根目錄的路徑
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
// 若是指定經過--offline方式,從本地緩存獲取模板
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}
console.log()
// 監聽用戶退出事件
process.on('exit', () => {
console.log()
})
//若是沒指定項目名稱或者要生成目錄的絕對路徑中已存在同名目錄
if (inPlace || exists(to)) {
// 詢問是否繼續生成
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
// 選擇繼續,繼續執行生成
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
// 已指定項目名和不存在同名目錄,直接執行
run()
}
複製代碼

下載模板邏輯

下載主要邏輯是判斷是否從緩存中的模板生成目錄,仍是第三方或者官方遠程倉庫讀取模板倉庫並進行下載,中間對以往廢棄的官方模板進行檢測,並進行提示。

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 { // 經過網絡安裝方式
//檢查是否存在新版本的vue-cli,固然這裏已經存在vue-cli更新
checkVersion(() => {
// 若是是官方模板
if (!hasSlash) {
// 指定官方模板git地址
const officialTemplate = 'vuejs-templates/' + template
// 模板名稱不存在'#'
if (template.indexOf('#') !== -1) {
//下載生成模板
downloadAndGenerate(officialTemplate)
} else {
//檢查模板名是帶有 '-2.0'
if (template.indexOf('-2.0') !== -1) {
//若是模板名存在`-2.0`,提示以前版本已廢棄,不需指定-2.0
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}
downloadAndGenerate(officialTemplate)
}
} else {
// 第三方模板下載
downloadAndGenerate(template)
}
})
}
}
複製代碼

這裏原先對於downloadAndGenerate對模板進行下載的時候對於本地緩存已存在的模板進行刪除比較迷惑,後來也想通了。多是做者想把腳手架和倉庫模板解耦,方便對模板倉庫隨時進行更新。這樣用戶就能夠每次經過網絡下載的時候獲得的都是最新的模板。


function downloadAndGenerate (template) {
//設置下載模板中提示信息
const spinner = ora('downloading template')
//顯示loading
spinner.start()
// 若是本地已存在模板,則進行刪除
if (exists(tmp)) rm(tmp)
// 下載模板名
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)
})
})
}
複製代碼

7 gernarate文件

Metalsmith是一個靜態網站生成器,主要的執行步驟包括讀取前端模板的meta.js或者meta.json模板配置(其中包括是否認義詢問的問題和文件過濾的配置),而後根據模板配置過濾不須要的類庫和文件,最後生成咱們想要的項目文件。

generate依賴

  • chalk: bash打印顏色高亮類庫
  • Metalsmith: 靜態網站生成器
  • Handlebars: 前端模板引擎,相似的還有ejs
  • async:支持異步處理的模塊
  • consolidate:支持各類模板引擎的渲染
  • multimatch:支持各類條件匹配的類庫
  • getOptions: 用於讀取meta.js中的模板配置
  • ask: 處理metajs中定義的詢問問題
  • filter:過濾文件
  • logger: 打印信息
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')
複製代碼

模板輔助函數和生成函數

在generate.js中爲模板定義模板渲染的輔助函數,並注入到模板配置中,這樣就可使用詢問收集的所需類庫來條件式渲染模板變量和過濾文件。

輔助函數

輔助函數能夠接受多個參數,最後一個參數 options 爲輔助函數的鉤子,調用 options.fn(this) 即輸出該輔助函數運算結果爲真時的內容,反之調用 options.inverse(this) 。

// 若是a和b兩個變量相等
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)
})

複製代碼

生成函數

傳入的參數name是表明建立的目錄名,src是模板的路徑,dest是項目生成的路徑,done是vue-init中定義的錯誤處理函數(若是發生錯誤就打印錯誤信息,成功就打印成功的信息)。


module.exports = function generate (name, src, dest, done) {
// 讀取模板的配置變量
const opts = getOptions(name, src)
// 初始化Metalsmith,這裏能夠看出vue模板定製的項目主要在template目錄
const metalsmith = Metalsmith(path.join(src, 'template'))
// 獲取metalsmith中的變量並注入一些自定義的變量
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
// 模板文件中是否存在定義的輔助函數,若是存在就遍歷註冊輔助函數
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})

const helpers = { chalk, logger }
// 若是模板配置中存在metalsmith.before函數,那麼執行該函數
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
// 詢問meta.js配置文件中定義的prompts定義好的問題
metalsmith.use(askQuestions(opts.prompts))
// 根據上一步收集好是否須要類庫的變量,根據條件過濾文件
.use(filterFiles(opts.filters))
// 渲染模板文件
.use(renderTemplateFiles(opts.skipInterpolation))
// meta.js中是否存在after函數,存在就執行
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)
// 從模板根目錄開始構建而不是`./ src`開始,後者是Metalsmith的`source`默認設置
.source('.')
// 在dest路徑生成項目文件
.destination(dest)
.build((err, files) => {
done(err)
// 若是配置文件中存在compltete函數就執行
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
} else {
//不然打印配置中定義的completeMessage函數
logMessage(opts.completeMessage, data)
}
})

return data
}
複製代碼

自定義metalsmith中間件

在generate中定義了兩個metalsmith中間件,一個是askQuestions (用於詢問問題),一個是filterFiles(用於根據條件過濾文件)

askQuestions

(files,metalsmith,done)是metalsmith中間件(插件)所需參數和格式,其中ask是自定義的函數,主要處理模板配置文件中的prompts問題。

function askQuestions (prompts) {
return (files, metalsmith, done) => {
ask(prompts, metalsmith.metadata(), done)
}
}
複製代碼

下面是ask.js內容

// 支持詢問問題的類型
const promptMapping = {
string: 'input',
boolean: 'confirm'
}
// prompts是配置中的詢問數組,data是metalsmith變量,done是處理函數(同上)
module.exports = function ask (prompts, data, done) {
async.eachSeries(Object.keys(prompts), (key, next) => {
prompt(data, key, prompts[key], next)
}, done)
}


function prompt (data, key, prompt, done) {
// 跳過不符合條件的提示,evaluate函數是在data的做用域執行exp表達式並返回其執行獲得的值
if (prompt.when && !evaluate(prompt.when, data)) {
return done()
}

let promptDefault = prompt.default
// 若是prompt的default值是一個函數,執行函數
if (typeof prompt.default === 'function') {
promptDefault = function () {
return prompt.default.bind(this)(data)
}
}
// inquirer收集問題答案
inquirer.prompt([{
type: promptMapping[prompt.type] || prompt.type,
name: key,
message: prompt.message || prompt.label || key,
default: promptDefault,
choices: prompt.choices || [],
validate: prompt.validate || (() => true)
}]).then(answers => {
if (Array.isArray(answers[key])) {
data[key] = {}
answers[key].forEach(multiChoiceAnswer => {
data[key][multiChoiceAnswer] = true
})
} else if (typeof answers[key] === 'string') {
data[key] = answers[key].replace(/"/g, '\\"')
} else {
data[key] = answers[key]
}
done()
}).catch(done)
}
複製代碼

filterFiles

filterFiles是根據ask.js收集的依賴變量,而後根據meta.js配置文件中定義的filter文件數組,判斷是否所需,來過濾不必的文件。

function filterFiles (filters) {
return (files, metalsmith, done) => {
filter(files, filters, metalsmith.metadata(), done)
}
}

// 用於條件匹配
const match = require('minimatch')
evaluate函數是在data的做用域執行exp表達式並返回其執行獲得的值
const evaluate = require('./eval')

module.exports = (files, filters, data, done) => {
// 若是沒有定義filters,直接執行done
if (!filters) {
return done()
}
// 獲取files的路徑
const fileNames = Object.keys(files)
//獲取filters數組定義的路徑key並遍歷
Object.keys(filters).forEach(glob => {
fileNames.forEach(file => {
// 若是files的路徑key和配置文件中定義的路徑key值匹配
if (match(file, glob, { dot: true })) {
// 獲取配置文件中定義的條件判斷value
const condition = filters[glob]
// 若是條件不匹配,就刪除metalsmith遍歷的file
if (!evaluate(condition, data)) {
delete files[file]
}
}
})
})
done()
}
複製代碼

renderTemplateFiles 渲染模板文件

renderTemplateFiles函數中定義了renderTemplateFiles,這個我在vue官方的webpack模板中沒有發現這個變量的定義,這裏主要用做定義須要跳過不處理的文件,renderTemplateFiles能夠是一個字符串路徑,也能夠是一個數組

function renderTemplateFiles (skipInterpolation) {
skipInterpolation = typeof skipInterpolation === 'string'
? [skipInterpolation]
: skipInterpolation
return (files, metalsmith, done) => {
const keys = Object.keys(files)
//獲取metalsmith的變量
const metalsmithMetadata = metalsmith.metadata()
// 異步處理,若是有定義須要跳過的文件,而且和當前遍歷的文件相匹配,則執行下一個
async.each(keys, (file, next) => {
// skipping files with skipInterpolation option
if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
return next()
}
//獲取當前遍歷文件file的內容(字符串形式)
const str = files[file].contents.toString()
// 若是當前文件內容不含有handlerbars的變量定義語法,則跳過該文件
if (!/{{([^{}]+)}}/g.test(str)) {
return next()
}
// 把文件中定義的變量替換成metalsmith的變量值
render(str, metalsmithMetadata, (err, res) => {
if (err) {
err.message = `[${file}] ${err.message}`
return next(err)
}
// 而後對文件內容從新賦值,這裏new Buffer內容已經被nodejs捨棄,能夠採用new Buffer.from(res)的用法
files[file].contents = new Buffer(res)
next()
})
}, done)
}
}
複製代碼

8 options讀取配置

  • read-metadata: 讀取meta.js的配置的庫
  • getGitUser: 獲取本地的git配置
  • validate-npm-package-name: 驗證npm包名
  • fs.existsSync: 判斷文件是否存在
// 讀取meta.js的配置
module.exports = function options (name, dir) {
const opts = getMetadata(dir)
// 設置prompt選項的默認值
setDefault(opts, 'name', name)
// 驗證name字段是否正確
setValidateName(opts)
// 獲取本地的git配置
const author = getGitUser()
// 若是git中定義了author,則設定做者的選項爲author提示
if (author) {
setDefault(opts, 'author', author)
}

return opts
}

function getMetadata (dir) {
// 配置文件能夠命名爲meta.json或者meta.js
const json = path.join(dir, 'meta.json')
const js = path.join(dir, 'meta.js')
let opts = {}

if (exists(json)) {
opts = metadata.sync(json)
} else if (exists(js)) {
const req = require(path.resolve(js))
if (req !== Object(req)) {
throw new Error('meta.js needs to expose an object')
}
opts = req
}

return opts
}
複製代碼

9 git-user

git-user主要使用了nodejs的exec運行本地git命令獲取git設置中的emailuser

const exec = require('child_process').execSync

module.exports = () => {
let name
let email

try {
name = exec('git config --get user.name')
email = exec('git config --get user.email')
} catch (e) {}

name = name && JSON.stringify(name.toString().trim()).slice(1, -1)
email = email && (' <' + email.toString().trim() + '>')
return (name || '') + (email || '')
}
複製代碼

總結上面介紹,主要對部分主要的代碼進行了解釋,對於一些感受比較好懂和對主流程不是過重要的代碼進行了刪減。你們在讀完以後,建議本身動手定製一個項目模板,本身手動定製了一個react + typescriptvue的項目模板。

點贊加關注

  1. 看到這裏了就點個贊支持下啦~,你的點贊,個人創做動力
  2. 關注公衆號「前端好talk」,好好說說前端經歷的故事
相關文章
相關標籤/搜索