vue-cli做了什么?(vue create)

目錄結(jié)構(gòu)

找到入口之package.json

vue-cli的目錄結(jié)構(gòu)|center

從上面的圖片中,大致可以看出,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)位置

packages的目錄結(jié)構(gòu)|center

可以看到@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)目的一步一步的提示和操作很相似嘛。


自定義插件創(chuàng)建項(xiàng)目|center

既然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)


resolveIntroPrompts返回了什么|center

再看下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)出來


自定義項(xiàng)目插件中的詢問器|center

接下來看一下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方法 我們看下流程圖


constructor流程|center
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中


Generator主要工作|center
// 配置插件默認(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 邏輯流程


create簡(jiǎn)要流程|center

可以看到的是,還是有很多我沒有提到的,像執(zhí)行install這種耗時(shí)較長(zhǎng)的操作的時(shí)候,使用單獨(dú)的進(jìn)程進(jìn)行處理,對(duì)整體代碼所使用的設(shè)計(jì)模式分析等等,針對(duì)create命令我們?cè)谥笤龠M(jìn)行補(bǔ)充。

我們學(xué)習(xí)到的

  1. JS傳參的時(shí)候,非基本類型的引用傳遞,方便我們實(shí)現(xiàn)控制反轉(zhuǎn)(DI)設(shè)計(jì)原則;
  2. OOP設(shè)計(jì)代碼中層級(jí)感會(huì)很好;
  3. Node.js狀態(tài)管理中我們也可以善用process.envs共享狀態(tài);
  4. 文件命名規(guī)范的重要性;
  5. 復(fù)雜軟件架構(gòu)的控制分離和業(yè)務(wù)解耦的思想,將generator的實(shí)際實(shí)現(xiàn)解耦在不同包中的generator中,提取出來所有的prompt等,vue-cli采用的插件式思想,可以使用注入思想實(shí)現(xiàn)對(duì)主流程的控制;
  6. 康威定理,大的系統(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)常留意下;
  7. 那么多引入的npm工具包都不知道怎么辦?不知道也是正常的,項(xiàng)目嘛,是需求和功能驅(qū)動(dòng)的,只有我們需要這個(gè)輪子的時(shí)候才能更快的接觸和了解這個(gè)輪子嘛;
  8. 冰山和設(shè)計(jì)模式中的外觀模式(Facade Pattern),不管我們寫多么復(fù)雜的軟件或者代碼,在遵循迪米特法則下,減少和整潔對(duì)外展現(xiàn),冰山下我們可以實(shí)現(xiàn)復(fù)雜,冰山之上要美觀和整潔。正如代碼一樣,實(shí)現(xiàn)的再是"屎山",只要實(shí)現(xiàn)功能,方便團(tuán)隊(duì)協(xié)作都是不錯(cuò)的代碼呢;
  9. 實(shí)現(xiàn)命令行其實(shí)不難,拿MVC分層設(shè)計(jì)來看,命令行也是隸屬于view層的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Nodejs除了賦予前端后端的能力外,還能有各種各樣的腳本,極大的簡(jiǎn)單各種操作。在早期,腳本做的工作大都是生成固定...
    MrTT閱讀 3,086評(píng)論 0 1
  • vue-cli是一種基于vue.js進(jìn)行快速開發(fā)的完整系統(tǒng),通過@vue/cli+ @vue/cli-servic...
    jluemmmm閱讀 538評(píng)論 0 0
  • Vue-CLI vue-cli是一種基于vue.js進(jìn)行快速開發(fā)的完整系統(tǒng),通過@vue/cli+ @vue/cl...
    jluemmmm閱讀 319評(píng)論 0 0
  • 1.項(xiàng)目搭建 1.命令行 vue create 項(xiàng)目名 2.選擇模版 (根據(jù)項(xiàng)目的需要進(jìn)行安裝,按步驟走下...
    Avery_G閱讀 1,511評(píng)論 1 2
  • vue-cli腳手架中webpack配置基礎(chǔ)文件詳解 一、前言 vue-cli是構(gòu)建vue單頁(yè)應(yīng)用的腳手架,輸入一...
    晟明閱讀 1,479評(píng)論 0 2

友情鏈接更多精彩內(nèi)容