目錄結(jié)構(gòu)
找到入口之package.json

從上面的圖片中,大致可以看出,packages是包的集合,scripts可能是腳本的集合,但是整體入口確不清楚在哪里。分析源碼的第一步,找到入口文件,先打開package.json看一下,這畢竟是一個(gè)npm項(xiàng)目的聲明文件。
{
"private": true,
"workspaces": [
"packages/@vue/*",
"packages/test/*",
"packages/vue-cli-version-marker"
],
"scripts": {
...
},
...
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"@vue/eslint-config-airbnb": "^5.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-standard": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vuepress/plugin-pwa": "^1.8.1",
"@vuepress/theme-vue": "^1.8.1",
"lerna": "^3.22.0",
...
},
...
}
上面是省略之后的package文件,我們知道vue-cli是一個(gè)多包聚合的項(xiàng)目,那么就必定會(huì)使用多包的管理依賴,從devDependencies確定vue-cli也是使用lerna進(jìn)行管理多包和版本的。lerna的特點(diǎn)是管理的包一般會(huì)收集在packages文件夾,并且需要新增lerna.json進(jìn)行packages的聲明,所以找到larna.json,查看一下:
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "5.0.0-beta.2",
"packages": [
"packages/@vue/babel-preset-app",
"packages/@vue/cli*",
"packages/vue-cli-version-marker"
],
"changelog": {
...
}
}
我們關(guān)注到,整體的包分為三大類,@vue/babel-preset-app預(yù)設(shè)相關(guān)的,@vue/cli主內(nèi)容,vue-cli-version-marker版本管理的。自此,從package.json的workspaces和lerna.json的packages字段中,可以發(fā)現(xiàn)主要的包其實(shí)是在package下的以@vue作為基本目錄的資源。我們將視線轉(zhuǎn)移至相應(yīng)位置

可以看到@vue下的包雖然挺多的,但是命名是很清晰明了的,我們大致可以猜出來,cli是集合可能是入口,cli-plugin應(yīng)該是實(shí)現(xiàn)cli的插件,cli-service,cli-ui等是我們用到的vue server和vue ui的實(shí)現(xiàn)。進(jìn)入cli,我們的任務(wù)是找到入口。cli是一個(gè)npm包,其目錄結(jié)構(gòu)大致是lib、bin、package.json等組成的,一樣的,我們從package.json開始。
{
"name": "@vue/cli",
"version": "5.0.0-beta.2",
"description": "Command line interface for rapid Vue.js development",
"bin": {
"vue": "bin/vue.js"
},
"types": "types/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue-cli.git",
"directory": "packages/@vue/cli"
},
"keywords": [
"vue",
"cli"
],
"author": "Evan You",
"license": "MIT",
...
"engines": {
"node": "^12.0.0 || >= 14.0.0"
}
}
大致瀏覽下,我們先不關(guān)心那么多,package.json中bin屬性是自定義命令的,其作用是提供在操作系統(tǒng)上的命令的link到指定文件的功能,也就是說,我們所能調(diào)用的命令,到最后其實(shí)調(diào)用到的是具體的文件,同時(shí)采用具體的解釋器(Python)、運(yùn)行時(shí)環(huán)境(Node.js)或其他程序進(jìn)行文件的執(zhí)行過程。可以看到,上面的package中的bin僅初測(cè)了一個(gè)vue的命令,導(dǎo)向的文件是bin下的vue.js,這就是vue xxx命令執(zhí)行的入口文件。
分析入口文件之vue.js
打開vue.js文件,我們逐行閱讀:本文分析過程采用深度優(yōu)先過程,我們遇到未知的就停下腳步進(jìn)行探索,之后再goto回來。
#!/usr/bin/env node
首行定義的是當(dāng)前文件的默認(rèn)執(zhí)行環(huán)境,上述的指定自然是使用node的環(huán)境,也就是說vue注冊(cè)的命令在實(shí)際執(zhí)行的時(shí)候,是link到vue.js這個(gè)文件,并用node進(jìn)行執(zhí)行的。
const { chalk, semver } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node
const leven = require('leven')
require了一些包而已,但是也是要注意一些知識(shí)的。
semver:The semantic versioner for npm. 提供的是npm包的版本比較等相關(guān)的功能;
chalk:Terminal string styling done right. 終端字體相關(guān)的庫(kù);
requiredVersion:導(dǎo)入了package中指定的node的版本控制字段;
leven:比較字符串之間的不相同字符數(shù)量。這個(gè)庫(kù)的實(shí)現(xiàn)其實(shí)是涉及到了萊茵斯坦算法的,該算法也是動(dòng)態(tài)規(guī)劃的一種具體使用的例子。我們先跳過去研究下:
插曲: 萊茵斯坦算法
萊茵斯坦算法是最短距離問題的一種,主要用在求一個(gè)文本轉(zhuǎn)換成另一個(gè)文本所需要的最小操作。算法將轉(zhuǎn)換操作分為插入(as->asd),刪除(asd->ad),替換(asd->add)三種,在比較轉(zhuǎn)換的時(shí)候采取的是動(dòng)態(tài)規(guī)劃的思想,假定兩個(gè)字符串A(0~i)長(zhǎng)度是I、B(0-j)長(zhǎng)度是J,我們要得出狀態(tài)轉(zhuǎn)移方程,設(shè)L(i,j)是A和B的最小萊茵斯坦距離,可以得出
L(i,j) = Min(L(i-1,j)+1,L(i,j-1)+1,L(i-1,j-1)+1) 當(dāng)Ai != Bj的時(shí)候
L(i,j) = Min(L(i-1,j)+1,L(i,j-1)+1,L(i-1,j-1)) 當(dāng) Ai === Bj的時(shí)候
本質(zhì)思想就是在,上一步的狀態(tài)會(huì)決定下一步的狀態(tài),在Ai === Bj在上一步的基礎(chǔ)上,那么就不用改變就能保證最小轉(zhuǎn)換。下面是leven庫(kù)的核心源碼:
'use strict';
const array = [];
const charCodeCache = [];
const leven = (left, right) => {
if (left === right) {
return 0;
}
const swap = left;
// Swapping the strings if `a` is longer than `b` so we know which one is the
// shortest & which one is the longest
// 保證最長(zhǎng)的字符串是在一側(cè)的 方便處理
if (left.length > right.length) {
left = right;
right = swap;
}
let leftLength = left.length;
let rightLength = right.length;
// Performing suffix trimming:
// We can linearly drop suffix common to both strings since they
// don't increase distance at all
// Note: `~-` is the bitwise way to perform a `- 1` operation
while (leftLength > 0 && (left.charCodeAt(~-leftLength) === right.charCodeAt(~-rightLength))) {
leftLength--;
rightLength--;
}
// Performing prefix trimming
// We can linearly drop prefix common to both strings since they
// don't increase distance at all
let start = 0;
while (start < leftLength && (left.charCodeAt(start) === right.charCodeAt(start))) {
start++;
}
leftLength -= start;
rightLength -= start;
if (leftLength === 0) {
return rightLength;
}
let bCharCode;
let result;
let temp;
let temp2;
let i = 0;
let j = 0;
while (i < leftLength) {
charCodeCache[i] = left.charCodeAt(start + i);
array[i] = ++i;
}
while (j < rightLength) {
bCharCode = right.charCodeAt(start + j);
temp = j++;
result = j;
for (i = 0; i < leftLength; i++) {
temp2 = bCharCode === charCodeCache[i] ? temp : temp + 1;
temp = array[i];
// eslint-disable-next-line no-multi-assign
// 一維數(shù)組存貯結(jié)果,此處進(jìn)行萊茵斯坦距離的計(jì)算,這里可以寫成Math.min(temp+1,result+1,temp2),為什么寫成三目運(yùn)算可能是因?yàn)檫@樣比較可以將概率最大的比較放在前面使得更快返回結(jié)果,而Math.min是一個(gè)O(n)的遍歷
result = array[i] = temp > result ? temp2 > result ? result + 1 : temp2 : temp2 > temp ? temp + 1 : temp2;
}
}
return result;
};
...
vue.js方法之checkNodeVersion
vue.js接著往下探索,是一個(gè)checkNodeVersion的函數(shù)聲明和調(diào)用,在package.json中的engins.node指定了最低版本,這里進(jìn)行檢查,不足則進(jìn)程退出。
function checkNodeVersion (wanted, id) {
if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
console.log(chalk.red(
'You are using Node ' + process.version + ', but this version of ' + id +
' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
))
process.exit(1)
}
}
checkNodeVersion(requiredVersion, '@vue/cli')
再往下,同樣是一系列的require,先忽略掉,slash:兼容不同環(huán)境的path格式化。再往下看,運(yùn)行測(cè)試用例的時(shí)候,開啟debug模式,debug模式的決定字段是在process.env中的,環(huán)境變量同樣可類似成多個(gè)包之間的一個(gè)公用的狀態(tài)管理器,同時(shí)惡可以看到vue-cli聲明的環(huán)境變量都是大寫+下劃線命名法加以區(qū)分的,這樣可以和process原本自帶的環(huán)境變量隔離開,我們自己實(shí)現(xiàn)代碼的時(shí)候也要注意這一點(diǎn),自己聲明的最好有著自己獨(dú)特的前綴,實(shí)現(xiàn)namespace的隔離思想。
// enter debug mode when creating test repo
if (
slash(process.cwd()).indexOf('/packages/test') > 0 && (
fs.existsSync(path.resolve(process.cwd(), '../@vue')) ||
fs.existsSync(path.resolve(process.cwd(), '../../@vue'))
)
) {
process.env.VUE_CLI_DEBUG = true
}
接著向下,兩個(gè)require聲明。我們注意到了commander庫(kù)的引入,這個(gè)就是注冊(cè)命令的庫(kù);loadCommand看起來是一個(gè)加載命令對(duì)應(yīng)文件的工具模塊。
const program = require('commander')
const loadCommand = require('../lib/util/loadCommand')
這里先大致說下commander庫(kù)的語(yǔ)法吧
program.option 注冊(cè)全局的命令option,option就是--opt或者-o這類的命令行傳參
program.version / .usage 聲明版本 / 聲明命令在終端的提示頭部信息
program.command 注冊(cè)命令
program.command.option 注冊(cè)命令中的option,傳參
program.command.action 終端命令敲完Enter的時(shí)候的執(zhí)行
好了,接著看代碼:
program
.version(`@vue/cli ${require('../package').version}`)
.usage('<command> [options]')
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.option('-g, --git [message]', 'Force git initialization with initial commit message')
.option('-n, --no-git', 'Skip git initialization')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('--merge', 'Merge target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
.option('-b, --bare', 'Scaffold project without beginner instructions')
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, options) => {
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
}
// --git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
require('../lib/create')(name, options)
})
上面這一部分聲明了vue create <name>的命令,同時(shí)指定了命令的參數(shù)(這么多屬性指定,我們直接引用vue-cli官方文檔上的解釋來確定create命令的部分需求):
-p, --preset <presetName> 忽略提示符并使用已保存的或遠(yuǎn)程的預(yù)設(shè)選項(xiàng)
-d, --default 忽略提示符并使用默認(rèn)預(yù)設(shè)選項(xiàng)
-i, --inlinePreset <json> 忽略提示符并使用內(nèi)聯(lián)的 JSON 字符串預(yù)設(shè)選項(xiàng)
-m, --packageManager <command> 在安裝依賴時(shí)使用指定的 npm 客戶端
-r, --registry <url> 在安裝依賴時(shí)使用指定的 npm registry
-g, --git [message] 強(qiáng)制 / 跳過 git 初始化,并可選的指定初始化提交信息
-n, --no-git 跳過 git 初始化
-f, --force 覆寫目標(biāo)目錄可能存在的配置
-c, --clone 使用 git clone 獲取遠(yuǎn)程預(yù)設(shè)選項(xiàng)
-x, --proxy 使用指定的代理創(chuàng)建項(xiàng)目
-b, --bare 創(chuàng)建項(xiàng)目時(shí)省略默認(rèn)組件中的新手指導(dǎo)信息
-h, --help 輸出使用幫助信息
預(yù)設(shè)preset一個(gè)不熟悉的概念,我們可以去官方文檔上看下作者想實(shí)現(xiàn)什么,https://cli.vuejs.org/zh/guide/plugins-and-presets.html#preset,指定的是自動(dòng)安裝的插件或者終端提示的插件交互。值得注意的是,-p提供的是本地和遠(yuǎn)程的preset信息的指定的。
我們具體看下上述代碼中的action的內(nèi)容
.action((name, options) => {
if (minimist(process.argv.slice(3))._.length > 1) {
// 命令輸入的時(shí)候參數(shù)不足的提醒
}
// --git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
// create命令的實(shí)際的執(zhí)行文件,直接引入并傳入name和options執(zhí)行
require('../lib/create')(name, options)
})
require是common.js規(guī)范中的引入模塊的方法,我們知道Node.js對(duì)文件require的時(shí)候原生是只支持.js、.node和.json文件的,下面可能會(huì)出現(xiàn)直接require文件夾的情況,處理如下:首先判斷文件夾下是否有package.json包聲明文件,有的話就執(zhí)行加載分析main屬性中的入口文件聲明,找到后就加載main指定的文件,若沒有main屬性或者沒有package.json文件,則默認(rèn)文件名為index,在文件夾當(dāng)前目錄下按照index.js、index.node、index.json進(jìn)行索引,找到的話就直接加載。
接下來的大篇幅代碼都是對(duì)各種命令的初始聲明了,我們先跳過,到較下方的位置
// output help information on unknown commands
// 對(duì)沒有注冊(cè)的命令進(jìn)行監(jiān)聽
program.on('command:*', ([cmd]) => {
program.outputHelp()
console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
console.log()
// 輸出已有的建議命令
suggestCommands(cmd)
process.exitCode = 1
})
// add some useful info on help
// 監(jiān)聽vue --help 命令
program.on('--help', () => {
...
})
// 遍歷所有命令 加入初始--help提示
program.commands.forEach(c => c.on('--help', () => console.log()))
看一下建議命令的suggestCommands方法
function suggestCommands (unknownCommand) {
// 收集到所有已經(jīng)注冊(cè)的命令
const availableCommands = program.commands.map(cmd => cmd._name)
let suggestion
// 找到最接近的命令
availableCommands.forEach(cmd => {
const isBestMatch = leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand)
if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
suggestion = cmd
}
})
// 輸出提示信息
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`))
}
}
接著代碼向下
// enhance common error messages
// 優(yōu)化命令錯(cuò)誤信息:丟失參數(shù),無法解釋的屬性值,丟失屬性值
const enhanceErrorMessages = require('../lib/util/enhanceErrorMessages')
enhanceErrorMessages('missingArgument', argName => {
return `Missing required argument ${chalk.yellow(`<${argName}>`)}.`
})
enhanceErrorMessages('unknownOption', optionName => {
return `Unknown option ${chalk.yellow(optionName)}.`
})
enhanceErrorMessages('optionMissingArgument', (option, flag) => {
return `Missing required argument for option ${chalk.yellow(option.flags)}` + (
flag ? `, got ${chalk.yellow(flag)}` : ``
)
})
// 給commander傳入args 其實(shí)commander正式運(yùn)行成功
program.parse(process.argv)
接下來跳轉(zhuǎn)到增強(qiáng)器的聲明文件看一下,lib/util/enhanceErrorMessages,
const program = require('commander')
const { chalk } = require('@vue/cli-shared-utils')
module.exports = (methodName, log) => {
program.Command.prototype[methodName] = function (...args) {
if (methodName === 'unknownOption' && this._allowUnknownOption) {
return
}
this.outputHelp()
console.log(` ` + chalk.red(log(...args)))
console.log()
process.exit(1)
}
}
很簡(jiǎn)短的幾行代碼啊,但是為什么又require('commander')了進(jìn)行設(shè)置就可以了呢?難道用的是單例模式?這個(gè)其實(shí)是require的又一個(gè)特性了,緩存機(jī)制,Node.js在實(shí)現(xiàn)require方法的時(shí)候,在require對(duì)象中有一個(gè)cache是專門保存require的方法的,只要第一次導(dǎo)入后的模塊,再此后再進(jìn)行加載的時(shí)候,會(huì)直接去cache中取到的,所以在這里再次取到的program就是從緩存數(shù)組中取出的,我們可以看下require的實(shí)際實(shí)現(xiàn)Module._load的代碼確定一下這個(gè)機(jī)制:
common.js之require
Module._load = function(request,// id parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain); // 加載當(dāng)前文件的模塊名
var cachedModule = Module._cache[filename]; // 查看緩存中是否存在該模塊
if (cachedModule) {
updateChildren(parent, cachedModule, true); // 存在的話就直接進(jìn)行當(dāng)前調(diào)用模塊的children處理
return cachedModule.exports; // 同時(shí)直接返回export對(duì)象
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent); // 創(chuàng)建新的模塊
if (isMain) {
process.mainModule = module;
module.id = '.';
} // 根模塊直接id賦值為空 同時(shí)把node的當(dāng)前主模塊指向
Module._cache[filename] = module;// 添加至緩存
tryModuleLoad(module, filename); // 加載模塊 其實(shí)就是使用IOIE形成作用域修改了module對(duì)象的export
return module.exports;
};
Module._load = function(request,// id parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain); // 加載當(dāng)前文件的模塊名
var cachedModule = Module._cache[filename]; // 查看緩存中是否存在該模塊
if (cachedModule) {
updateChildren(parent, cachedModule, true); // 存在的話就直接進(jìn)行當(dāng)前調(diào)用模塊的children處理
return cachedModule.exports; // 同時(shí)直接返回export對(duì)象
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent); // 創(chuàng)建新的模塊
if (isMain) {
process.mainModule = module;
module.id = '.';
} // 根模塊直接id賦值為空 同時(shí)把node的當(dāng)前主模塊指向
Module._cache[filename] = module;// 添加至緩存
tryModuleLoad(module, filename); // 加載模塊 其實(shí)就是使用IOIE形成作用域修改了module對(duì)象的export
return module.exports;
};
其大致的執(zhí)行流程就是:Module._load -> tryModuleLoad -> Module.load -> Module._compile -> V8.Module._compile,最終的compile其實(shí)就是在require的文件對(duì)象的前后拼接上"(function (exports, require, module, __filename, __dirname) {" 和末位的"})"之后由v8中的runInContext進(jìn)行組裝后的函數(shù)執(zhí)行,模塊中定義的module.exports執(zhí)行后就賦值到了傳入的形參module對(duì)象上了,由此就完成了對(duì)象或者值導(dǎo)出的傳遞,最終將module.exports返回完成導(dǎo)入,這里也是為什么我們不能在模塊中直接寫出來module = xxx的這種代碼,這就直接會(huì)導(dǎo)致待導(dǎo)出的值和形參中傳入的Module對(duì)象直接脫離關(guān)系。common.js是由模塊隔離出來的作用域,而作用域其實(shí)就是最終執(zhí)行中傳輸?shù)腗odule對(duì)象。這里的理解可以類比原始導(dǎo)入script腳本的思想的,我們手動(dòng)實(shí)現(xiàn)script的導(dǎo)入的時(shí)候,為了實(shí)現(xiàn)不同腳本之間的變量隔離,可以先聲明一個(gè)對(duì)象A,執(zhí)行腳本后將腳本中暴露的所有變量都變成A的屬性,最終在實(shí)際執(zhí)行的腳本中,就實(shí)現(xiàn)了不同script的變量隔離了。
分析create命令實(shí)現(xiàn)之create.js
上述中找到了實(shí)現(xiàn)create命令的文件,是在lib下的create.js實(shí)現(xiàn)的,我們對(duì)其進(jìn)行分析
const fs = require('fs-extra')
const path = require('path')
const inquirer = require('inquirer')
const Creator = require('./Creator')
const { clearConsole } = require('./util/clearConsole')
const { getPromptModules } = require('./util/createTools')
const { chalk, error, stopSpinner, exit } = require('@vue/cli-shared-utils')
const validateProjectName = require('validate-npm-package-name')
大致先看下引入的模塊,其中的Creator就在create.js同級(jí)目錄下,可以看出,實(shí)現(xiàn)create的主要邏輯是多個(gè)文件的呢,我們先記錄下,在用到Creator對(duì)象的時(shí)候進(jìn)行分析。其他模塊我們根據(jù)語(yǔ)義化的命令,大致可以知道inquirer(終端命令),clearConsole清理終端顯示,getPromptModules獲取Prompt模塊,validateProjectName判斷名稱合法性。
既然這是個(gè)模塊,那么我們先找到exports的定義:
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false) // do not persist
error(err)
if (!process.env.VUE_CLI_TEST) {
process.exit(1)
}
})
}
可以看到實(shí)質(zhì)上就是執(zhí)行了create方法,進(jìn)入create function
async function create (projectName, options) {
// 代理聲明
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}
const cwd = options.cwd || process.cwd()
const inCurrent = projectName === '.'
// 項(xiàng)目名稱初始化 可以看到 我們名稱可以只寫個(gè) . 的 這樣就用上級(jí)目錄的目錄名作為項(xiàng)目名
const name = inCurrent ? path.relative('../', cwd) : projectName
// 目標(biāo)目錄,基目錄是運(yùn)行命令所在的目錄
const targetDir = path.resolve(cwd, projectName || '.')
// 項(xiàng)目名稱要符合npm包名的規(guī)范 同時(shí)也不能和已經(jīng)有的包名重復(fù)
const result = validateProjectName(name)
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
exit(1)
}
// 當(dāng)前目錄已經(jīng)存在的話 同時(shí)不是merge代碼的時(shí)候 需要查看force屬性值/提示用戶復(fù)寫目錄(刪除原目錄)
if (fs.existsSync(targetDir) && !options.merge) {
if (options.force) {
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
// 創(chuàng)建一個(gè)Createor 傳入項(xiàng)目名稱,項(xiàng)目的目錄,預(yù)設(shè)的依賴 同時(shí)執(zhí)行其create方法
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
可以看到,流程是很清晰的,proxy參數(shù)為true的時(shí)候改變環(huán)境變量 -> 確定項(xiàng)目名稱和項(xiàng)目目錄 -> 目錄中不為空的時(shí)候,詢問用戶是否刪除重新創(chuàng)建空目錄 -> new Creator同時(shí)執(zhí)行create方法。不過在new Creator的時(shí)候,發(fā)現(xiàn)傳入了getPromptModules(),我們先看看這個(gè)方法做了什么
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
可以看到,是對(duì)一些字符串做了遍歷require的操作,字符串甚至還有點(diǎn)熟悉,這和我們create項(xiàng)目的一步一步的提示和操作很相似嘛。

既然require了promptModules中的文件,我們?nèi)ゲ榭聪挛募惺窃趺炊x的,以vueVersion為例:
module.exports = cli => {
cli.injectFeature({
name: 'Choose Vue version',
value: 'vueVersion',
description: 'Choose a version of Vue.js that you want to start the project with',
checked: true
})
cli.injectPrompt({
name: 'vueVersion',
when: answers => answers.features.includes('vueVersion'),
message: 'Choose a version of Vue.js that you want to start the project with',
type: 'list',
choices: [
{
name: '2.x',
value: '2'
},
{
name: '3.x',
value: '3'
}
],
default: '2'
})
cli.onPromptComplete((answers, options) => {
if (answers.vueVersion) {
options.vueVersion = answers.vueVersion
}
})
}
可以看到,定義的就是inquirer看使用的終端選擇提示器,用戶可對(duì)涉及選擇的插件的加載進(jìn)行自定義的選擇操作。
實(shí)現(xiàn)create.js主要功能之Creator.js
一上來同樣的是require的聲明
const path = require('path')
const debug = require('debug')
const inquirer = require('inquirer')
const EventEmitter = require('events')
const Generator = require('./Generator')
const cloneDeep = require('lodash.clonedeep')
const sortObject = require('./util/sortObject')
const getVersions = require('./util/getVersions')
const PackageManager = require('./util/ProjectPackageManager')
const { clearConsole } = require('./util/clearConsole')
const PromptModuleAPI = require('./PromptModuleAPI')
const writeFileTree = require('./util/writeFileTree')
const { formatFeatures } = require('./util/features')
const loadLocalPreset = require('./util/loadLocalPreset')
const loadRemotePreset = require('./util/loadRemotePreset')
const generateReadme = require('./util/generateReadme')
const { resolvePkg, isOfficialPlugin } = require('@vue/cli-shared-utils')
仔細(xì)查看,我們也能學(xué)到一些加載模塊方面的優(yōu)化,像是lodash的包,是提供按需加載的,只需要clonedeep方法的時(shí)候可以通過require(lodash.clonedeep)進(jìn)行單一方法的導(dǎo)入。
再往下,導(dǎo)入了同目錄下的option.js模塊
const {
defaults,
saveOptions,
loadOptions,
savePreset,
validatePreset,
rcPath
} = require('./options')
這個(gè)模塊提供了不少的配置項(xiàng),我們先看下實(shí)現(xiàn):
const fs = require('fs')
const cloneDeep = require('lodash.clonedeep')
const { getRcPath } = require('./util/rcPath')
const { exit } = require('@vue/cli-shared-utils/lib/exit')
const { error } = require('@vue/cli-shared-utils/lib/logger')
const { createSchema, validate } = require('@vue/cli-shared-utils/lib/validate')
const rcPath = exports.rcPath = getRcPath('.vuerc')
const presetSchema ...
const schema ... // 定義校驗(yàn)規(guī)則
exports.validatePreset = preset => validate(preset, presetSchema, msg => {
error(`invalid preset options: ${msg}`)
})
// 默認(rèn)預(yù)設(shè)
exports.defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
// defaults主要針對(duì)vue的版本不同 改變其presets中的vueVersion屬性值,默認(rèn)的插件是babel和eslint。同時(shí)提供了和vuerc文件一樣的數(shù)據(jù)結(jié)構(gòu)
exports.defaults = {
lastChecked: undefined,
latestVersion: undefined,
packageManager: undefined,
useTaobaoRegistry: undefined,
presets: {
'default': Object.assign({ vueVersion: '2' }, exports.defaultPreset),
'__default_vue_3__': Object.assign({ vueVersion: '3' }, exports.defaultPreset)
}
}
let cachedOptions
exports.loadOptions = () => {
if (cachedOptions) {
return cachedOptions
}
// 如同vue-cli官方文檔中提到的,在 vue create 過程中保存的 preset 會(huì)被放在你的 home 目錄下的一個(gè)配置文件中 (~/.vuerc)。你可以通過直接編輯這個(gè)文件來調(diào)整、添加、刪除保存好的 preset
// 此處就判斷是否存在,存在則直接讀出文件內(nèi)容
if (fs.existsSync(rcPath)) {
try {
cachedOptions = JSON.parse(fs.readFileSync(rcPath, 'utf-8'))
} catch (e) {
error(
`Error loading saved preferences: ` +
`~/.vuerc may be corrupted or have syntax errors. ` +
`Please fix/delete it and re-run vue-cli in manual mode.\n` +
`(${e.message})`
)
exit(1)
}
// 校驗(yàn)cachedOptions是否滿足schema的限制條件,不滿足則提示outdated
validate(cachedOptions, schema, () => {
error(
`~/.vuerc may be outdated. ` +
`Please delete it and re-run vue-cli in manual mode.`
)
})
return cachedOptions
} else {
return {}
}
}
exports.saveOptions = toSave => {
// 匯集默認(rèn)和用戶自定義的預(yù)設(shè)項(xiàng)
const options = Object.assign(cloneDeep(exports.loadOptions()), toSave)
// 移除無關(guān)屬性
for (const key in options) {
if (!(key in exports.defaults)) {
delete options[key]
}
}
// 覆蓋~/.veurc文件
cachedOptions = options
try {
fs.writeFileSync(rcPath, JSON.stringify(options, null, 2))
return true
} catch (e) {
error(
`Error saving preferences: ` +
`make sure you have write access to ${rcPath}.\n` +
`(${e.message})`
)
}
}
// 修改.vuerc中的presets屬性
exports.savePreset = (name, preset) => {
const presets = cloneDeep(exports.loadOptions().presets || {})
presets[name] = preset
return exports.saveOptions({ presets })
}
由此我們得出options這個(gè)文件主要是獲取默認(rèn)的vuerc文件對(duì)象和覆寫vuerc文件內(nèi)容的方法。
Creator.js再往下,是對(duì)@vue/cli-shared-utils工具方法的引入
const {
chalk,
execa,
log,
warn,
error,
hasGit,
hasProjectGit,
hasYarn,
hasPnpm3OrLater,
hasPnpmVersionOrLater,
exit,
loadModule
} = require('@vue/cli-shared-utils')
接下來的代碼比較長(zhǎng)了,我們從class的定義入手
module.exports = class Creator extends EventEmitter
可以看到Creator是繼承了EventEmitter,可以實(shí)現(xiàn)出來on/once的方法了都
// name項(xiàng)目名稱,context項(xiàng)目所在的目錄(pwd+name),promptModules默認(rèn)顯示的可配置項(xiàng)目(回想一下上面的["vueVersion",...].map的方法)
constructor (name, context, promptModules) {
super()
this.name = name
this.context = process.env.VUE_CLI_CONTEXT = context
// 返回預(yù)設(shè)方案的終端選擇器 自定義方案的插件預(yù)選器 1
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
// 返回其他的預(yù)設(shè)選擇器 2
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
this.promptCompleteCbs = []
this.afterInvokeCbs = []
this.afterAnyInvokeCbs = []
this.run = this.run.bind(this)
// DI模式 將promptModules中的injectFeature等事件的操作轉(zhuǎn)換成對(duì)this.creator屬性中的操作 3
// 隨著遍歷執(zhí)行完成 this.featurePrompt.choices就將待選的插件都注入進(jìn)去
// 同時(shí)this.injectedPrompts和this.promptCompleteCbs也注入進(jìn)去了插件的實(shí)現(xiàn)
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
}
這里面有三個(gè)點(diǎn)是調(diào)用了其他函數(shù)了,我們逐個(gè)看下,先看下resolveIntroPrompts
// 返回的是兩個(gè)終端選擇功能,選擇預(yù)設(shè)的方案,自定義的時(shí)候選擇自動(dòng)安裝的插件
resolveIntroPrompts () {
const presets = this.getPresets()
// 格式化預(yù)設(shè)對(duì)象 輸出數(shù)組 形如
// [
// {
// name: "Default([Vue2], babel, eslint)",
// value: "default"
// }
// ]
const presetChoices = Object.entries(presets).map(([name, preset]) => {
let displayName = name
// 置換default和__default_vue_3__的name
if (name === 'default') {
displayName = 'Default'
} else if (name === '__default_vue_3__') {
displayName = 'Default (Vue 3)'
}
return {
name: `${displayName} (${formatFeatures(preset)})`,
value: name
}
})
// 提示用戶選擇craete的預(yù)設(shè)流程,Default ([Vue 2] babel, eslint)/Default (Vue 3) ([Vue 3] babel, eslint)/Manually select features
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__'
}
]
}
// 選擇Manually select features后顯示待選擇插件 choices初始化為空數(shù)組
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
上面看起來可能比較抽象,我們直接根據(jù)結(jié)果展示來反推下上述的實(shí)現(xiàn)

再看下resolveOutroPrompts方法
// 返回其他的預(yù)設(shè)選擇器
resolveOutroPrompts () {
const outroPrompts = [
{
name: 'useConfigFiles',
when: isManualMode,
type: 'list',
message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
choices: [
{
name: 'In dedicated config files',
value: 'files'
},
{
name: 'In package.json',
value: 'pkg'
}
]
},
{
name: 'save',
when: isManualMode,
type: 'confirm',
message: 'Save this as a preset for future projects?',
default: false
},
{
name: 'saveName',
when: answers => answers.save,
type: 'input',
message: 'Save preset as:'
}
]
// ask for packageManager once
const savedOptions = loadOptions()
if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
const packageManagerChoices = []
if (hasYarn()) {
packageManagerChoices.push({
name: 'Use Yarn',
value: 'yarn',
short: 'Yarn'
})
}
if (hasPnpm3OrLater()) {
packageManagerChoices.push({
name: 'Use PNPM',
value: 'pnpm',
short: 'PNPM'
})
}
packageManagerChoices.push({
name: 'Use NPM',
value: 'npm',
short: 'NPM'
})
outroPrompts.push({
name: 'packageManager',
type: 'list',
message: 'Pick the package manager to use when installing dependencies:',
choices: packageManagerChoices
})
}
return outroPrompts
}
一如既往的抽象,但是可以看出,這個(gè)是在我們選擇Manually select features后的可能提示,在命令行中我們也可以試驗(yàn)出來

接下來看一下PromptModuleAPI的巧妙實(shí)現(xiàn),SOLID原則告訴我們高層的策略實(shí)現(xiàn)不應(yīng)當(dāng)依賴于底層的細(xì)節(jié)實(shí)現(xiàn),但是底層的細(xì)節(jié)實(shí)現(xiàn)是可以依賴高層的策略實(shí)現(xiàn)的。我們看下API這個(gè)文件是怎么實(shí)現(xiàn)的,這個(gè)文件需要和我們之前看過的vueVersion.js這個(gè)一起對(duì)比
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
API類主要實(shí)現(xiàn)的是對(duì)Creator內(nèi)部數(shù)組變量的操作,可以看作一個(gè)抽象類,而promptModule中的vueVersion.js等文件相當(dāng)于是抽象類的具體實(shí)現(xiàn)。所以后面在執(zhí)行到promptModules.forEach(m => m(promptAPI))的時(shí)候,就把實(shí)現(xiàn)中的各個(gè)部分都注入到了當(dāng)前Creator中來了。
同時(shí)constructor定義了run函數(shù),執(zhí)行OS上的shell命令。
Creator.create
前面調(diào)用的時(shí)候就發(fā)現(xiàn)。Creator new出來之后就開始執(zhí)行create方法了,我們接著看create方法,看來主要的業(yè)務(wù)邏輯都在這里
async create (cliOptions = {}, preset = null) {
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
// 按照不同的預(yù)設(shè)進(jìn)行不同的加載過程 defaul/--preset/inlinePreset
if (!preset) {
if (cliOptions.preset) {
// vue create foo --preset bar
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else if (cliOptions.default) {
// vue create foo --default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) {
// vue create foo --inlinePreset {...}
try {
preset = JSON.parse(cliOptions.inlinePreset)
} catch (e) {
error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
exit(1)
}
} else {
preset = await this.promptAndResolvePreset()
}
}
// clone before mutating
preset = cloneDeep(preset)
// inject core service
// @vue/cli-service插件中保存所有的預(yù)設(shè)信息和項(xiàng)目名稱
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
// 回憶一下-b的功能: 忽略新手指導(dǎo)信息
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
// 安裝router插件的初始化 不指定的化是hash router
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// legacy support for vuex
// 配置vuex的插件初始信息
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
// 識(shí)別包管理工具 可見優(yōu)先級(jí)是從用戶指定到.vuerc默認(rèn)再到y(tǒng)arn和npm等的兜底
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
await clearConsole()
// 新建出來包管理對(duì)象 后續(xù)install等都是這個(gè)對(duì)象提供的方法
const pm = new PackageManager({ context, forcePackageManager: packageManager })
log(`? Creating project in ${chalk.yellow(context)}.`)
// creating鉤子 嘖嘖嘖 繼承EventEmitter的必要性
this.emit('creation', { event: 'creating' })
// get latest CLI plugin version
const { latestMinor } = await getVersions()
// generate package.json with plugin dependencies
// 這里生成的就是默認(rèn)package.json的對(duì)象
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context)
}
// 遍歷操作預(yù)設(shè)/自定義的plugins
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
if (preset.plugins[dep]._isPreset) {
return
}
let { version } = preset.plugins[dep]
// 插件版本號(hào)處理
// 沒有對(duì)插件指定版本號(hào) 則使用最新的版本 latest;vue-service和babel-preset-env這是腳手架實(shí)現(xiàn)的,正式環(huán)境版本都由package指定
if (!version) {
// @vue開頭的 都會(huì)受到package中的version聲明影響
if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
version = isTestOrDebug ? `latest` : `~${latestMinor}`
} else {
version = 'latest'
}
}
pkg.devDependencies[dep] = version
})
// write package.json
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
// 使用pnpm管理的話 則需要在.npmrc中指定shamefully-hoist/shamefully-flatten
if (packageManager === 'pnpm') {
const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
? 'shamefully-hoist=true\n'
: 'shamefully-flatten=true\n'
await writeFileTree(context, {
'.npmrc': pnpmConfig
})
}
// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
// 這里的重點(diǎn)也是對(duì)git的判斷上了
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
log(`?? Initializing git repository...`)
this.emit('creation', { event: 'git-init' })
await run('git init')
}
// install plugins
// 準(zhǔn)備開始install plugin了,理論上我們也可以在這個(gè)鉤子這里對(duì)插件的版本注入操作
log(`?\u{fe0f} Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install' })
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
// in development, avoid installation process
await require('./util/setupDevProject')(context)
} else {
// install插件了 也可以直接run執(zhí)行 不過npm實(shí)現(xiàn)的是有對(duì)接的庫(kù)的
await pm.install()
}
// run generator
// 生產(chǎn)器
log(`?? Invoking generators...`)
this.emit('creation', { event: 'invoking-generators' })
// 合并 獲取到所有引入的npm包
const plugins = await this.resolvePlugins(preset.plugins, pkg)
// ※ 重要類 Generator 等下跳轉(zhuǎn)過去看實(shí)現(xiàn)
// 傳入的參數(shù) package.json的對(duì)象,引入的npm包集合,執(zhí)行完Generator的回調(diào)數(shù)組,執(zhí)行每一項(xiàng)invoke的回調(diào)數(shù)組
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
// 直接執(zhí)行了generate方法 傳入了預(yù)設(shè)中的useConfigFiles的設(shè)置
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
// install additional deps (injected by generators)
log(`?? Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install' })
log()
// 又一次install 那么Generator預(yù)計(jì)可能修改了待需要install的包數(shù)組呢
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
await pm.install()
}
// run complete cbs if any (injected by generators)
log(`? Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' })
// 給了competion生命周期 鉤子函數(shù)的調(diào)用機(jī)會(huì)
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
// 生成README文件
if (!generator.files['README.md']) {
// generate README.md
log()
log('?? Generating README.md...')
await writeFileTree(context, {
'README.md': generateReadme(generator.pkg, packageManager)
})
}
// commit initial state
// 默認(rèn)使用git配置的時(shí)候 腳手架會(huì)在生成項(xiàng)目之后就執(zhí)行一次 add和commit
let gitCommitFailed = false
if (shouldInitGit) {
await run('git add -A')
if (isTestOrDebug) {
await run('git', ['config', 'user.name', 'test'])
await run('git', ['config', 'user.email', 'test@test.com'])
await run('git', ['config', 'commit.gpgSign', 'false'])
}
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {
await run('git', ['commit', '-m', msg, '--no-verify'])
} catch (e) {
gitCommitFailed = true
}
}
// log instructions
log()
log(`?? Successfully created project ${chalk.yellow(name)}.`)
if (!cliOptions.skipGetStarted) {
log(
`?? Get started with the following commands:\n\n` +
(this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
)
}
log()
this.emit('creation', { event: 'done' })
// 這里可以看出,git commit失敗也不能影響構(gòu)建流程??低诙ɡ硭? 時(shí)間再多一件事情也不可能做的完美,但總有時(shí)間做完一件事情。即使不夠完美,也要先保證主要流程是成功的。
if (gitCommitFailed) {
warn(
`Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
`You will need to perform the initial commit yourself.\n`
)
}
generator.printExitLogs()
}
resolvePreset
// 獲取.vuerc中指定的預(yù)設(shè)和默認(rèn)預(yù)設(shè)的集合 結(jié)果形如
// {
// 'default': Object.assign({ vueVersion: '2' }, exports.defaultPreset),
// '__default_vue_3__': Object.assign({ vueVersion: '3' }, exports.defaultPreset)
// }
getPresets () {
const savedOptions = loadOptions()
return Object.assign({}, savedOptions.presets, defaults.presets)
}
async resolvePreset (name, clone) {
let preset
const savedPresets = this.getPresets()
if (name in savedPresets) {
// 已經(jīng)保存過的預(yù)設(shè)方案直接索引到就可以了
preset = savedPresets[name]
} else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {
// 命令行--preset聲明的是json文件 、絕對(duì)/相對(duì)路徑信息等 需要加載解析對(duì)應(yīng)文件
preset = await loadLocalPreset(path.resolve(name))
} else if (name.includes('/')) {
// 名字中含有/的就去遠(yuǎn)端加載了
log(`Fetching remote preset ${chalk.cyan(name)}...`)
this.emit('creation', { event: 'fetch-remote-preset' })
try {
preset = await loadRemotePreset(name, clone)
} catch (e) {
error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
throw e
}
}
// 錯(cuò)誤處理 上述的分支都沒命中
if (!preset) {
error(`preset "${name}" not found.`)
const presets = Object.keys(savedPresets)
if (presets.length) {
log()
log(`available presets:\n${presets.join(`\n`)}`)
} else {
log(`you don't seem to have any saved preset.`)
log(`run vue-cli in manual mode to create a preset.`)
}
exit(1)
}
return preset
}
PackageManager
我們先分析下PackageManager的核心需求:執(zhí)行install。再進(jìn)一步,核心需求的質(zhì)量需求:1 兼容不同包管理工具 2 非阻塞 3 友好的錯(cuò)誤信息。此時(shí)我們?cè)倏创a的實(shí)現(xiàn):先只關(guān)心構(gòu)造器和install方法 我們看下流程圖

class PackageManager {
constructor ({ context, forcePackageManager } = {}) {
this.context = context || process.cwd()
this._registries = {}
// 確定包管理工具
if (forcePackageManager) {
this.bin = forcePackageManager
} else if (context) {
if (hasProjectYarn(context)) {
this.bin = 'yarn'
} else if (hasProjectPnpm(context)) {
this.bin = 'pnpm'
} else if (hasProjectNpm(context)) {
this.bin = 'npm'
}
}
// if no package managers specified, and no lockfile exists
if (!this.bin) {
this.bin = loadOptions().packageManager || (hasYarn() ? 'yarn' : hasPnpm3OrLater() ? 'pnpm' : 'npm')
}
// 不支持較低版本的npm
if (this.bin === 'npm') {
// npm doesn't support package aliases until v6.9
const MIN_SUPPORTED_NPM_VERSION = '6.9.0'
const npmVersion = stripAnsi(execa.sync('npm', ['--version']).stdout)
if (semver.lt(npmVersion, MIN_SUPPORTED_NPM_VERSION)) {
throw new Error(
'You are using an outdated version of NPM.\n' +
'It does not support some core functionalities of Vue CLI.\n' +
'Please upgrade your NPM version.'
)
}
if (semver.gte(npmVersion, '7.0.0')) {
this.needsPeerDepsFix = true
}
}
// 兜底方案還是npm執(zhí)行
if (!SUPPORTED_PACKAGE_MANAGERS.includes(this.bin)) {
log()
warn(
`The package manager ${chalk.red(this.bin)} is ${chalk.red('not officially supported')}.\n` +
`It will be treated like ${chalk.cyan('npm')}, but compatibility issues may occur.\n` +
`See if you can use ${chalk.cyan('--registry')} instead.`
)
PACKAGE_MANAGER_CONFIG[this.bin] = PACKAGE_MANAGER_CONFIG.npm
}
// Plugin may be located in another location if `resolveFrom` presents.
const projectPkg = resolvePkg(this.context)
const resolveFrom = projectPkg && projectPkg.vuePlugins && projectPkg.vuePlugins.resolveFrom
// Logically, `resolveFrom` and `context` are distinct fields.
// But in Vue CLI we only care about plugins.
// So it is fine to let all other operations take place in the `resolveFrom` directory.
if (resolveFrom) {
this.context = path.resolve(context, resolveFrom)
}
}
async install () {
const args = []
// npm大于7.0.0的都符合了 只有6.9.0 - 7.0.0才沒有
if (this.needsPeerDepsFix) {
args.push('--legacy-peer-deps')
}
if (process.env.VUE_CLI_TEST) {
args.push('--silent', '--no-progress')
}
return await this.runCommand('install', args)
}
async runCommand (command, args) {
const prevNodeEnv = process.env.NODE_ENV
// In the use case of Vue CLI, when installing dependencies,
// the `NODE_ENV` environment variable does no good;
// it only confuses users by skipping dev deps (when set to `production`).
delete process.env.NODE_ENV
// 獲取包管理源 設(shè)置npm_config_registry YARN_NPM_REGISTRY_SERVER 設(shè)置鏡像源
await this.setRegistryEnvs()
// 執(zhí)行install
await executeCommand(
this.bin,
[
...PACKAGE_MANAGER_CONFIG[this.bin][command],
...(args || [])
],
this.context
)
if (prevNodeEnv) {
process.env.NODE_ENV = prevNodeEnv
}
}
// Any command that implemented registry-related feature should support
// `-r` / `--registry` option
// 確定包管理源
async getRegistry (scope) {
const cacheKey = scope || ''
if (this._registries[cacheKey]) {
return this._registries[cacheKey]
}
const args = minimist(process.argv, {
alias: {
r: 'registry'
}
})
let registry
if (args.registry) {
registry = args.registry
} else if (!process.env.VUE_CLI_TEST && await shouldUseTaobao(this.bin)) {
registry = registries.taobao
} else {
try {
if (scope) {
registry = (await execa(this.bin, ['config', 'get', scope + ':registry'])).stdout
}
if (!registry || registry === 'undefined') {
registry = (await execa(this.bin, ['config', 'get', 'registry'])).stdout
}
} catch (e) {
// Yarn 2 uses `npmRegistryServer` instead of `registry`
registry = (await execa(this.bin, ['config', 'get', 'npmRegistryServer'])).stdout
}
}
this._registries[cacheKey] = stripAnsi(registry).trim()
return this._registries[cacheKey]
}
async getAuthConfig (scope) {
// get npmrc (https://docs.npmjs.com/configuring-npm/npmrc.html#files)
const possibleRcPaths = [
path.resolve(this.context, '.npmrc'),
path.resolve(require('os').homedir(), '.npmrc')
]
if (process.env.PREFIX) {
possibleRcPaths.push(path.resolve(process.env.PREFIX, '/etc/npmrc'))
}
// there's also a '/path/to/npm/npmrc', skipped for simplicity of implementation
let npmConfig = {}
for (const loc of possibleRcPaths) {
if (fs.existsSync(loc)) {
try {
// the closer config file (the one with lower index) takes higher precedence
npmConfig = Object.assign({}, ini.parse(fs.readFileSync(loc, 'utf-8')), npmConfig)
} catch (e) {
// in case of file permission issues, etc.
}
}
}
const registry = await this.getRegistry(scope)
const registryWithoutProtocol = registry
.replace(/https?:/, '') // remove leading protocol
.replace(/([^/])$/, '$1/') // ensure ending with slash
const authTokenKey = `${registryWithoutProtocol}:_authToken`
const authUsernameKey = `${registryWithoutProtocol}:username`
const authPasswordKey = `${registryWithoutProtocol}:_password`
const auth = {}
if (authTokenKey in npmConfig) {
auth.token = npmConfig[authTokenKey]
}
if (authPasswordKey in npmConfig) {
auth.username = npmConfig[authUsernameKey]
auth.password = Buffer.from(npmConfig[authPasswordKey], 'base64').toString()
}
return auth
}
async setRegistryEnvs () {
// 包管理源
const registry = await this.getRegistry()
// 環(huán)境變量設(shè)置
process.env.npm_config_registry = registry
process.env.YARN_NPM_REGISTRY_SERVER = registry
this.setBinaryMirrors()
}
// set mirror urls for users in china
// 設(shè)置鏡像源
async setBinaryMirrors () {
const registry = await this.getRegistry()
if (registry !== registries.taobao) {
return
}
try {
// chromedriver, etc.
const binaryMirrorConfigMetadata = await this.getMetadata('binary-mirror-config', { full: true })
const latest = binaryMirrorConfigMetadata['dist-tags'] && binaryMirrorConfigMetadata['dist-tags'].latest
const mirrors = binaryMirrorConfigMetadata.versions[latest].mirrors.china
for (const key in mirrors.ENVS) {
process.env[key] = mirrors.ENVS[key]
}
// Cypress
const cypressMirror = mirrors.cypress
const defaultPlatforms = {
darwin: 'osx64',
linux: 'linux64',
win32: 'win64'
}
const platforms = cypressMirror.newPlatforms || defaultPlatforms
const targetPlatform = platforms[require('os').platform()]
// Do not override user-defined env variable
// Because we may construct a wrong download url and an escape hatch is necessary
if (targetPlatform && !process.env.CYPRESS_INSTALL_BINARY) {
const projectPkg = resolvePkg(this.context)
if (projectPkg && projectPkg.devDependencies && projectPkg.devDependencies.cypress) {
const wantedCypressVersion = await this.getRemoteVersion('cypress', projectPkg.devDependencies.cypress)
process.env.CYPRESS_INSTALL_BINARY =
`${cypressMirror.host}/${wantedCypressVersion}/${targetPlatform}/cypress.zip`
}
}
} catch (e) {
// get binary mirror config failed
}
}
}
isOfficialPlugin
isOfficialPlugin就能判斷出來是否是@vue開頭內(nèi)置插件
const officialRE = /^@vue\//
exports.isOfficialPlugin = id => exports.isPlugin(id) && officialRE.test(id)
Generator
最后的一項(xiàng)復(fù)雜的類Generator 文件生成器,主要是由useConfigFiles來控制的,主要作用是將package中定義的插件的配置項(xiàng)都轉(zhuǎn)移到對(duì)應(yīng)的包管理文件中,舉個(gè)例子,package中配置babel的配置信息之后,會(huì)進(jìn)行轉(zhuǎn)移保存到.babelrc或者babel.cofig.js中

// 配置插件默認(rèn)的配置文件信息
const defaultConfigTransforms = {
babel: new ConfigTransform({
file: {
js: ['babel.config.js']
}
}),
postcss: new ConfigTransform({
file: {
js: ['postcss.config.js'],
json: ['.postcssrc.json', '.postcssrc'],
yaml: ['.postcssrc.yaml', '.postcssrc.yml']
}
}),
eslintConfig: new ConfigTransform({
file: {
js: ['.eslintrc.js'],
json: ['.eslintrc', '.eslintrc.json'],
yaml: ['.eslintrc.yaml', '.eslintrc.yml']
}
}),
jest: new ConfigTransform({
file: {
js: ['jest.config.js']
}
}),
browserslist: new ConfigTransform({
file: {
lines: ['.browserslistrc']
}
}),
'lint-staged': new ConfigTransform({
file: {
js: ['lint-staged.config.js'],
json: ['.lintstagedrc', '.lintstagedrc.json'],
yaml: ['.lintstagedrc.yaml', '.lintstagedrc.yml']
}
})
}
const reservedConfigTransforms = {
vue: new ConfigTransform({
file: {
js: ['vue.config.js']
}
})
module.exports = class Generator {
constructor (context, {
pkg = {},
plugins = [],
afterInvokeCbs = [],
afterAnyInvokeCbs = [],
files = {},
invoking = false
} = {}) {
this.context = context
this.plugins = sortPlugins(plugins)
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
this.imports = {}
this.rootOptions = {}
this.afterInvokeCbs = afterInvokeCbs
this.afterAnyInvokeCbs = afterAnyInvokeCbs
this.configTransforms = {}
this.defaultConfigTransforms = defaultConfigTransforms
this.reservedConfigTransforms = reservedConfigTransforms
this.invoking = invoking
// for conflict resolution
this.depSources = {}
// virtual file tree
// 待寫入的文件列表
this.files = Object.keys(files).length
// when execute `vue add/invoke`, only created/modified files are written to disk
? watchFiles(files, this.filesModifyRecord = new Set())
// all files need to be written to disk
: files
this.fileMiddlewares = []
this.postProcessFilesCbs = []
// exit messages
this.exitLogs = []
// load all the other plugins
// 獲取到所有的依賴包 格式是
// {
// apply: #require的值
// id
// }
this.allPlugins = this.resolveAllPlugins()
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService
? cliService.options
: inferRootOptions(pkg)
this.rootOptions = rootOptions
}
// 初始化所有待安裝的依賴包
async initPlugins () {
const { rootOptions, invoking } = this
const pluginIds = this.plugins.map(p => p.id)
// avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
// 這里重新賦值this.afterInvokeCbs的作用是 在引入的包中如果包需要引入其他的子依賴或者其他操作的時(shí)候 會(huì)執(zhí)行hooks中的方法 注入GeneratorAPI對(duì)象改變this.afterInvokeCbs
// 同理 對(duì)下面的this.plugins的this.afterAnyInvokeCbs重新賦值也是基于這個(gè)道理的
const passedAfterInvokeCbs = this.afterInvokeCbs
this.afterInvokeCbs = []
// apply hooks from all plugins to collect 'afterAnyHooks'
for (const plugin of this.allPlugins) {
const { id, apply } = plugin
const api = new GeneratorAPI(id, this, {}, rootOptions)
if (apply.hooks) {
await apply.hooks(api, {}, rootOptions, pluginIds)
}
}
// We are doing save/load to make the hook order deterministic
// save "any" hooks
const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
// reset hooks
this.afterInvokeCbs = passedAfterInvokeCbs
this.afterAnyInvokeCbs = []
this.postProcessFilesCbs = []
// apply generators from plugins
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions, invoking)
if (apply.hooks) {
// while we execute the entire `hooks` function,
// only the `afterInvoke` hook is respected
// because `afterAnyHooks` is already determined by the `allPlugins` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
// restore "any" hooks
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
}
async generate ({
extractConfigFiles = false,
checkExisting = false
} = {}) {
// 初始化所有要依賴的包 追加依賴包中的依賴問題
await this.initPlugins()
// save the file system before applying plugin for comparison
const initialFiles = Object.assign({}, this.files)
// extract configs from package.json into dedicated files.
// 這里做的事情是對(duì)pkg中的對(duì)包的配置項(xiàng)提出去到對(duì)應(yīng)的文件 并且移除掉pkg中的配置
// 寫入文件
this.extractConfigFiles(extractConfigFiles, checkExisting)
// wait for file resolve
await this.resolveFiles()
// set package.json
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// write/update file tree to disk
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
// 將pkg對(duì)象中的配置包的信息 提取到對(duì)應(yīng)的文件中 并保存file對(duì)象列表
extractConfigFiles (extractAll, checkExisting) {
// 收集可能是包配置的config的name集合
const configTransforms = Object.assign({},
defaultConfigTransforms,
this.configTransforms,
reservedConfigTransforms
)
// 定義提取方法 判斷可能是對(duì)包的配置信息的話 執(zhí)行transfrom方法 將文件內(nèi)容生成/追加 并存入files數(shù)組 同時(shí)移除pkg原有的配置項(xiàng)
const extract = key => {
if (
configTransforms[key] &&
this.pkg[key] &&
// do not extract if the field exists in original package.json
!this.originalPkg[key]
) {
const value = this.pkg[key]
const configTransform = configTransforms[key]
const res = configTransform.transform(
value,
checkExisting,
this.files,
this.context
)
const { content, filename } = res
this.files[filename] = ensureEOL(content)
delete this.pkg[key]
}
}
// 判斷是否提取所有的包 不是的話 就只提取vue和babel的配置項(xiàng) 即生成/修改vue.config.js和babel.config.js文件
if (extractAll) {
for (const key in this.pkg) {
extract(key)
}
} else {
if (!process.env.VUE_CLI_TEST) {
// by default, always extract vue.config.js
extract('vue')
}
// always extract babel.config.js as this is the only way to apply
// project-wide configuration even to dependencies.
// TODO: this can be removed when Babel supports root: true in package.json
extract('babel')
}
}
...
}
GeneratorAPI
GeneratorAPI主要做的事情是修改Generator類中的回調(diào)方法的數(shù)組,在plugins中聲明hooks可以通過注入GeneratorAPI對(duì)象來修改Generator中的屬性值。其主要定義了一系列的修改Generator實(shí)例對(duì)象屬性的方法,對(duì)應(yīng)plugin包的generator的hooks中實(shí)現(xiàn)即可。
class GeneratorAPI {
/**
* @param {string} id - Id of the owner plugin
* @param {Generator} generator - The invoking Generator instance
* @param {object} options - generator options passed to this plugin
* @param {object} rootOptions - root options (the entire preset)
*/
constructor (id, generator, options, rootOptions) {
this.id = id
this.generator = generator
this.options = options
this.rootOptions = rootOptions
/* eslint-disable no-shadow */
this.pluginsData = generator.plugins
.filter(({ id }) => id !== `@vue/cli-service`)
.map(({ id }) => ({
name: toShortPluginId(id),
link: getPluginLink(id)
}))
/* eslint-enable no-shadow */
this._entryFile = undefined
}
/**
* Resolves the data when rendering templates.
*
* @private
*/
_resolveData (additionalData) {
return Object.assign({
options: this.options,
rootOptions: this.rootOptions,
plugins: this.pluginsData
}, additionalData)
}
/**
* Inject a file processing middleware.
*
* @private
* @param {FileMiddleware} middleware - A middleware function that receives the
* virtual files tree object, and an ejs render function. Can be async.
*/
_injectFileMiddleware (middleware) {
this.generator.fileMiddlewares.push(middleware)
}
/**
* Normalize absolute path, Windows-style path
* to the relative path used as index in this.files
* @param {string} p the path to normalize
*/
_normalizePath (p) {
if (path.isAbsolute(p)) {
p = path.relative(this.generator.context, p)
}
// The `files` tree always use `/` in its index.
// So we need to normalize the path string in case the user passes a Windows path.
return p.replace(/\\/g, '/')
}
/**
* Resolve path for a project.
*
* @param {string} _paths - A sequence of relative paths or path segments
* @return {string} The resolved absolute path, caculated based on the current project root.
*/
resolve (..._paths) {
return path.resolve(this.generator.context, ..._paths)
}
get cliVersion () {
return require('../package.json').version
}
assertCliVersion (range) {
if (typeof range === 'number') {
if (!Number.isInteger(range)) {
throw new Error('Expected string or integer value.')
}
range = `^${range}.0.0-0`
}
if (typeof range !== 'string') {
throw new Error('Expected string or integer value.')
}
if (semver.satisfies(this.cliVersion, range, { includePrerelease: true })) return
throw new Error(
`Require global @vue/cli "${range}", but was invoked by "${this.cliVersion}".`
)
}
get cliServiceVersion () {
// In generator unit tests, we don't write the actual file back to the disk.
// So there is no cli-service module to load.
// In that case, just return the cli version.
if (process.env.VUE_CLI_TEST && process.env.VUE_CLI_SKIP_WRITE) {
return this.cliVersion
}
const servicePkg = loadModule(
'@vue/cli-service/package.json',
this.generator.context
)
return servicePkg.version
}
assertCliServiceVersion (range) {
if (typeof range === 'number') {
if (!Number.isInteger(range)) {
throw new Error('Expected string or integer value.')
}
range = `^${range}.0.0-0`
}
if (typeof range !== 'string') {
throw new Error('Expected string or integer value.')
}
if (semver.satisfies(this.cliServiceVersion, range, { includePrerelease: true })) return
throw new Error(
`Require @vue/cli-service "${range}", but was loaded with "${this.cliServiceVersion}".`
)
}
/**
* Check if the project has a given plugin.
*
* @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
* @param {string} version - Plugin version. Defaults to ''
* @return {boolean}
*/
hasPlugin (id, versionRange) {
return this.generator.hasPlugin(id, versionRange)
}
/**
* Configure how config files are extracted.
*
* @param {string} key - Config key in package.json
* @param {object} options - Options
* @param {object} options.file - File descriptor
* Used to search for existing file.
* Each key is a file type (possible values: ['js', 'json', 'yaml', 'lines']).
* The value is a list of filenames.
* Example:
* {
* js: ['.eslintrc.js'],
* json: ['.eslintrc.json', '.eslintrc']
* }
* By default, the first filename will be used to create the config file.
*/
addConfigTransform (key, options) {
const hasReserved = Object.keys(this.generator.reservedConfigTransforms).includes(key)
if (
hasReserved ||
!options ||
!options.file
) {
if (hasReserved) {
const { warn } = require('@vue/cli-shared-utils')
warn(`Reserved config transform '${key}'`)
}
return
}
this.generator.configTransforms[key] = new ConfigTransform(options)
}
/**
* Extend the package.json of the project.
* Also resolves dependency conflicts between plugins.
* Tool configuration fields may be extracted into standalone files before
* files are written to disk.
*
* @param {object | () => object} fields - Fields to merge.
* @param {object} [options] - Options for extending / merging fields.
* @param {boolean} [options.prune=false] - Remove null or undefined fields
* from the object after merging.
* @param {boolean} [options.merge=true] deep-merge nested fields, note
* that dependency fields are always deep merged regardless of this option.
* @param {boolean} [options.warnIncompatibleVersions=true] Output warning
* if two dependency version ranges don't intersect.
* @param {boolean} [options.forceOverwrite=false] force using the dependency
* version provided in the first argument, instead of trying to get the newer ones
*/
extendPackage (fields, options = {}) {
const extendOptions = {
prune: false,
merge: true,
warnIncompatibleVersions: true,
forceOverwrite: false
}
// this condition statement is added for compatibility reason, because
// in version 4.0.0 to 4.1.2, there's no `options` object, but a `forceNewVersion` flag
if (typeof options === 'boolean') {
extendOptions.warnIncompatibleVersions = !options
} else {
Object.assign(extendOptions, options)
}
const pkg = this.generator.pkg
const toMerge = isFunction(fields) ? fields(pkg) : fields
for (const key in toMerge) {
const value = toMerge[key]
const existing = pkg[key]
if (isObject(value) && (key === 'dependencies' || key === 'devDependencies')) {
// use special version resolution merge
pkg[key] = mergeDeps(
this.id,
existing || {},
value,
this.generator.depSources,
extendOptions
)
} else if (!extendOptions.merge || !(key in pkg)) {
pkg[key] = value
} else if (Array.isArray(value) && Array.isArray(existing)) {
pkg[key] = mergeArrayWithDedupe(existing, value)
} else if (isObject(value) && isObject(existing)) {
pkg[key] = deepmerge(existing, value, { arrayMerge: mergeArrayWithDedupe })
} else {
pkg[key] = value
}
}
if (extendOptions.prune) {
pruneObject(pkg)
}
}
/**
* Render template files into the virtual files tree object.
*
* @param {string | object | FileMiddleware} source -
* Can be one of:
* - relative path to a directory;
* - Object hash of { sourceTemplate: targetFile } mappings;
* - a custom file middleware function.
* @param {object} [additionalData] - additional data available to templates.
* @param {object} [ejsOptions] - options for ejs.
*/
render (source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
if (isString(source)) {
source = path.resolve(baseDir, source)
this._injectFileMiddleware(async (files) => {
const data = this._resolveData(additionalData)
const globby = require('globby')
const _files = await globby(['**/*'], { cwd: source, dot: true })
for (const rawPath of _files) {
const targetPath = rawPath.split('/').map(filename => {
// dotfiles are ignored when published to npm, therefore in templates
// we need to use underscore instead (e.g. "_gitignore")
if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
return `.${filename.slice(1)}`
}
if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {
return `${filename.slice(1)}`
}
return filename
}).join('/')
const sourcePath = path.resolve(source, rawPath)
const content = renderFile(sourcePath, data, ejsOptions)
// only set file if it's not all whitespace, or is a Buffer (binary files)
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
files[targetPath] = content
}
}
})
} else if (isObject(source)) {
this._injectFileMiddleware(files => {
const data = this._resolveData(additionalData)
for (const targetPath in source) {
const sourcePath = path.resolve(baseDir, source[targetPath])
const content = renderFile(sourcePath, data, ejsOptions)
if (Buffer.isBuffer(content) || content.trim()) {
files[targetPath] = content
}
}
})
} else if (isFunction(source)) {
this._injectFileMiddleware(source)
}
}
/**
* Push a file middleware that will be applied after all normal file
* middelwares have been applied.
*
* @param {FileMiddleware} cb
*/
postProcessFiles (cb) {
this.generator.postProcessFilesCbs.push(cb)
}
/**
* Push a callback to be called when the files have been written to disk.
*
* @param {function} cb
*/
onCreateComplete (cb) {
this.afterInvoke(cb)
}
afterInvoke (cb) {
this.generator.afterInvokeCbs.push(cb)
}
/**
* Push a callback to be called when the files have been written to disk
* from non invoked plugins
*
* @param {function} cb
*/
afterAnyInvoke (cb) {
this.generator.afterAnyInvokeCbs.push(cb)
}
/**
* Add a message to be printed when the generator exits (after any other standard messages).
*
* @param {} msg String or value to print after the generation is completed
* @param {('log'|'info'|'done'|'warn'|'error')} [type='log'] Type of message
*/
exitLog (msg, type = 'log') {
this.generator.exitLogs.push({ id: this.id, msg, type })
}
/**
* convenience method for generating a js config file from json
*/
genJSConfig (value) {
return `module.exports = ${stringifyJS(value, null, 2)}`
}
/**
* Turns a string expression into executable JS for JS configs.
* @param {*} str JS expression as a string
*/
makeJSOnlyValue (str) {
const fn = () => {}
fn.__expression = str
return fn
}
/**
* Run codemod on a script file or the script part of a .vue file
* @param {string} file the path to the file to transform
* @param {Codemod} codemod the codemod module to run
* @param {object} options additional options for the codemod
*/
transformScript (file, codemod, options) {
const normalizedPath = this._normalizePath(file)
this._injectFileMiddleware(files => {
if (typeof files[normalizedPath] === 'undefined') {
error(`Cannot find file ${normalizedPath}`)
return
}
files[normalizedPath] = runTransformation(
{
path: this.resolve(normalizedPath),
source: files[normalizedPath]
},
codemod,
options
)
})
}
/**
* Add import statements to a file.
*/
injectImports (file, imports) {
const _imports = (
this.generator.imports[file] ||
(this.generator.imports[file] = new Set())
)
;(Array.isArray(imports) ? imports : [imports]).forEach(imp => {
_imports.add(imp)
})
}
/**
* Add options to the root Vue instance (detected by `new Vue`).
*/
injectRootOptions (file, options) {
const _options = (
this.generator.rootOptions[file] ||
(this.generator.rootOptions[file] = new Set())
)
;(Array.isArray(options) ? options : [options]).forEach(opt => {
_options.add(opt)
})
}
/**
* Get the entry file taking into account typescript.
*
* @readonly
*/
get entryFile () {
if (this._entryFile) return this._entryFile
return (this._entryFile = fs.existsSync(this.resolve('src/main.ts')) ? 'src/main.ts' : 'src/main.js')
}
/**
* Is the plugin being invoked?
*
* @readonly
*/
get invoking () {
return this.generator.invoking
}
}
總結(jié)vue create
自此,對(duì)create命令的代碼一覽大致告一段落了,接下來我們總結(jié)下其主要流程。
1 文件調(diào)用線
create.js -> Creator.js -> Generator.js(GeneratorAPI)
2 邏輯流程

可以看到的是,還是有很多我沒有提到的,像執(zhí)行install這種耗時(shí)較長(zhǎng)的操作的時(shí)候,使用單獨(dú)的進(jìn)程進(jìn)行處理,對(duì)整體代碼所使用的設(shè)計(jì)模式分析等等,針對(duì)create命令我們?cè)谥笤龠M(jìn)行補(bǔ)充。
我們學(xué)習(xí)到的
- JS傳參的時(shí)候,非基本類型的引用傳遞,方便我們實(shí)現(xiàn)控制反轉(zhuǎn)(DI)設(shè)計(jì)原則;
- OOP設(shè)計(jì)代碼中層級(jí)感會(huì)很好;
- Node.js狀態(tài)管理中我們也可以善用process.envs共享狀態(tài);
- 文件命名規(guī)范的重要性;
- 復(fù)雜軟件架構(gòu)的控制分離和業(yè)務(wù)解耦的思想,將generator的實(shí)際實(shí)現(xiàn)解耦在不同包中的generator中,提取出來所有的prompt等,vue-cli采用的插件式思想,可以使用注入思想實(shí)現(xiàn)對(duì)主流程的控制;
- 康威定理,大的系統(tǒng)組織總是比小系統(tǒng)更傾向于分解。vue-cli在我寫這篇文件的時(shí)候也進(jìn)行了一次更新,重新簡(jiǎn)化了craetor.js等主邏輯文件中的代碼,將次要邏輯和實(shí)現(xiàn)都抽離在了單獨(dú)的文件中。我們從中也可以學(xué)習(xí)到在工作中對(duì)代碼重構(gòu)的思想,軟件在大型化之后就不得不面臨著拆分,合理的拆分和解耦就是重構(gòu)中不可或缺的一部分,善用善思考軟件設(shè)計(jì)很重要,從需求、架構(gòu)、軟件開發(fā)方法、設(shè)計(jì)原則、設(shè)計(jì)模式等各個(gè)方法深入思考,不管是采用DDD為主的以業(yè)務(wù)為核心分離思想,還是Clean-Architecture的簡(jiǎn)潔架構(gòu),其實(shí)都是為了實(shí)現(xiàn)在開發(fā)和維護(hù)中針對(duì)需求變化中的代碼最小變動(dòng),這一點(diǎn)我們?cè)谌粘i_發(fā)中可以經(jīng)常留意下;
- 那么多引入的npm工具包都不知道怎么辦?不知道也是正常的,項(xiàng)目嘛,是需求和功能驅(qū)動(dòng)的,只有我們需要這個(gè)輪子的時(shí)候才能更快的接觸和了解這個(gè)輪子嘛;
- 冰山和設(shè)計(jì)模式中的外觀模式(Facade Pattern),不管我們寫多么復(fù)雜的軟件或者代碼,在遵循迪米特法則下,減少和整潔對(duì)外展現(xiàn),冰山下我們可以實(shí)現(xiàn)復(fù)雜,冰山之上要美觀和整潔。正如代碼一樣,實(shí)現(xiàn)的再是"屎山",只要實(shí)現(xiàn)功能,方便團(tuán)隊(duì)協(xié)作都是不錯(cuò)的代碼呢;
- 實(shí)現(xiàn)命令行其實(shí)不難,拿MVC分層設(shè)計(jì)來看,命令行也是隸屬于view層的。