最近在學習 vue-cli 的源碼,獲益良多。爲了讓本身理解得更加深入,我決定模仿它造一個輪子,爭取儘量多的實現原有的功能。html
我將這個輪子分紅三個版本:前端
有人可能不懂腳手架是什麼。按個人理解,腳手架就是幫助你把項目的基礎架子搭好。例如項目依賴、模板、構建工具等等。讓你不用從零開始配置一個項目,儘量快的進行業務開發。vue
建議在閱讀本文時,可以結合項目源碼一塊兒配合使用,效果更好。這是項目地址 mini-cli。項目中的每個分支都對應一個版本,例如第一個版本對應的 git 分支爲 v1。因此在閱讀源碼時,記得要切換到對應的分支。node
第一個版本的功能比較簡單,大體爲:webpack
package.json
文件,並添加對應的依賴項。index.html
、main.js
、App.vue
等文件)。npm install
命令安裝依賴。項目目錄樹:git
├─.vscode ├─bin │ ├─mvc.js # mvc 全局命令 ├─lib │ ├─generator # 各個功能的模板 │ │ ├─babel # babel 模板 │ │ ├─linter # eslint 模板 │ │ ├─router # vue-router 模板 │ │ ├─vue # vue 模板 │ │ ├─vuex # vuex 模板 │ │ └─webpack # webpack 模板 │ ├─promptModules # 各個模塊的交互提示語 │ └─utils # 一系列工具函數 │ ├─create.js # create 命令處理函數 │ ├─Creator.js # 處理交互提示 │ ├─Generator.js # 渲染模板 │ ├─PromptModuleAPI.js # 將各個功能的提示語注入 Creator └─scripts # commit message 驗證腳本 和項目無關 不需關注
腳手架第一個功能就是處理用戶的命令,這須要使用 commander.js。這個庫的功能就是解析用戶的命令,提取出用戶的輸入交給腳手架。例如這段代碼:github
#!/usr/bin/env node const program = require('commander') const create = require('../lib/create') program .version('0.1.0') .command('create <name>') .description('create a new project') .action(name => { create(name) }) program.parse()
它使用 commander 註冊了一個 create
命令,並設置了腳手架的版本和描述。我將這段代碼保存在項目下的 bin
目錄,並命名爲 mvc.js
。而後在 package.json
文件添加這段代碼:web
"bin": { "mvc": "./bin/mvc.js" },
再執行 npm link,就能夠將 mvc
註冊成全局命令。這樣在電腦上的任何地方都能使用 mvc
命令了。實際上,就是用 mvc
命令來代替執行 node ./bin/mvc.js
。vue-router
假設用戶在命令行上輸入 mvc create demo
(實際上執行的是 node ./bin/mvc.js create demo
),commander
解析到命令 create
和參數 demo
。而後腳手架能夠在 action
回調裏取到參數 name
(值爲 demo)。vuex
取到用戶要建立的項目名稱 demo
以後,就能夠彈出交互選項,詢問用戶要建立的項目須要哪些功能。這須要用到 [
Inquirer.js](https://github.com/SBoudrias/...。Inquirer.js
的功能就是彈出一個問題和一些選項,讓用戶選擇。而且選項能夠指定是多選、單選等等。
例以下面的代碼:
const prompts = [ { "name": "features", // 選項名稱 "message": "Check the features needed for your project:", // 選項提示語 "pageSize": 10, "type": "checkbox", // 選項類型 另外還有 confirm list 等 "choices": [ // 具體的選項 { "name": "Babel", "value": "babel", "short": "Babel", "description": "Transpile modern JavaScript to older versions (for compatibility)", "link": "https://babeljs.io/", "checked": true }, { "name": "Router", "value": "router", "description": "Structure the app with dynamic pages", "link": "https://router.vuejs.org/" }, ] } ] inquirer.prompt(prompts)
彈出的問題和選項以下:
問題的類型 "type": "checkbox"
是 checkbox
說明是多選。若是兩個選項都進行選中的話,返回來的值爲:
{ features: ['babel', 'router'] }
其中 features
是上面問題中的 name
屬性。features
數組中的值則是每一個選項中的 value
。
Inquirer.js
還能夠提供具備相關性的問題,也就是上一個問題選擇了指定的選項,下一個問題纔會顯示出來。例以下面的代碼:
{ name: 'Router', value: 'router', description: 'Structure the app with dynamic pages', link: 'https://router.vuejs.org/', }, { name: 'historyMode', when: answers => answers.features.includes('router'), type: 'confirm', message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`, description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`, link: 'https://router.vuejs.org/guide/essentials/history-mode.html', },
第二個問題中有一個屬性 when
,它的值是一個函數 answers => answers.features.includes('router')
。當函數的執行結果爲 true
,第二個問題纔會顯示出來。若是你在上一個問題中選擇了 router
,它的結果就會變爲 true
。彈出第二個問題:問你路由模式是否選擇 history
模式。
大體瞭解 Inquirer.js
後,就能夠明白這一步咱們要作什麼了。主要就是將腳手架支持的功能配合對應的問題、可選值在控制檯上展現出來,供用戶選擇。獲取到用戶具體的選項值後,再渲染模板和依賴。
先來看一下第一個版本支持哪些功能:
因爲這是一個 vue 相關的腳手架,因此 vue 是默認提供的,不須要用戶選擇。另外構建工具 webpack 提供了開發環境和打包的功能,也是必需的,不用用戶進行選擇。因此可供用戶選擇的功能只有 4 個:
如今咱們先來看一下這 4 個功能對應的交互提示語相關的文件。它們所有放在 lib/promptModules
目錄下:
-babel.js -linter.js -router.js -vuex.js
每一個文件包含了和它相關的全部交互式問題。例如剛纔的示例,說明 router
相關的問題有兩個。下面再看一下 babel.js
的代碼:
module.exports = (api) => { api.injectFeature({ name: 'Babel', value: 'babel', short: 'Babel', description: 'Transpile modern JavaScript to older versions (for compatibility)', link: 'https://babeljs.io/', checked: true, }) }
只有一個問題,就是問下用戶需不須要 babel
功能,默認爲 checked: true
,也就是須要。
用戶使用 create
命令後,腳手架須要將全部功能的交互提示語句聚合在一塊兒:
// craete.js const creator = new Creator() // 獲取各個模塊的交互提示語 const promptModules = getPromptModules() const promptAPI = new PromptModuleAPI(creator) promptModules.forEach(m => m(promptAPI)) // 清空控制檯 clearConsole() // 彈出交互提示語並獲取用戶的選擇 const answers = await inquirer.prompt(creator.getFinalPrompts()) function getPromptModules() { return [ 'babel', 'router', 'vuex', 'linter', ].map(file => require(`./promptModules/${file}`)) } // Creator.js class Creator { constructor() { this.featurePrompt = { name: 'features', message: 'Check the features needed for your project:', pageSize: 10, type: 'checkbox', choices: [], } this.injectedPrompts = [] } getFinalPrompts() { this.injectedPrompts.forEach(prompt => { const originalWhen = prompt.when || (() => true) prompt.when = answers => originalWhen(answers) }) const prompts = [ this.featurePrompt, ...this.injectedPrompts, ] return prompts } } module.exports = Creator // PromptModuleAPI.js module.exports = class PromptModuleAPI { constructor(creator) { this.creator = creator } injectFeature(feature) { this.creator.featurePrompt.choices.push(feature) } injectPrompt(prompt) { this.creator.injectedPrompts.push(prompt) } }
以上代碼的邏輯以下:
creator
對象getPromptModules()
獲取全部功能的交互提示語PromptModuleAPI
將全部交互提示語注入到 creator
對象const answers = await inquirer.prompt(creator.getFinalPrompts())
在控制檯彈出交互語句,並將用戶選擇結果賦值給 answers
變量。若是全部功能都選上,answers
的值爲:
{ features: [ 'vue', 'webpack', 'babel', 'router', 'vuex', 'linter' ], // 項目具備的功能 historyMode: true, // 路由是否使用 history 模式 eslintConfig: 'airbnb', // esilnt 校驗代碼的默認規則,可被覆蓋 lintOn: [ 'save' ] // 保存代碼時進行校驗 }
獲取用戶的選項後就該開始渲染模板和生成 package.json
文件了。先來看一下如何生成 package.json
文件:
// package.json 文件內容 const pkg = { name, version: '0.1.0', dependencies: {}, devDependencies: {}, }
先定義一個 pkg
變量來表示 package.json
文件,並設定一些默認值。
全部的項目模板都放在 lib/generator
目錄下:
├─lib │ ├─generator # 各個功能的模板 │ │ ├─babel # babel 模板 │ │ ├─linter # eslint 模板 │ │ ├─router # vue-router 模板 │ │ ├─vue # vue 模板 │ │ ├─vuex # vuex 模板 │ │ └─webpack # webpack 模板
每一個模板的功能都差很少:
pkg
變量注入依賴項下面是 babel
相關的代碼:
module.exports = (generator) => { generator.extendPackage({ babel: { presets: ['@babel/preset-env'], }, dependencies: { 'core-js': '^3.8.3', }, devDependencies: { '@babel/core': '^7.12.13', '@babel/preset-env': '^7.12.13', 'babel-loader': '^8.2.2', }, }) }
能夠看到,模板調用 generator
對象的 extendPackage()
方法向 pkg
變量注入了 babel
相關的全部依賴。
extendPackage(fields) { const pkg = this.pkg for (const key in fields) { const value = fields[key] const existing = pkg[key] if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) { pkg[key] = Object.assign(existing || {}, value) } else { pkg[key] = value } } }
注入依賴的過程就是遍歷全部用戶已選擇的模板,並調用 extendPackage()
注入依賴。
腳手架是怎麼渲染模板的呢?用 vuex
舉例,先看一下它的代碼:
module.exports = (generator) => { // 向入口文件 `src/main.js` 注入代碼 import store from './store' generator.injectImports(generator.entryFile, `import store from './store'`) // 向入口文件 `src/main.js` 的 new Vue() 注入選項 store generator.injectRootOptions(generator.entryFile, `store`) // 注入依賴 generator.extendPackage({ dependencies: { vuex: '^3.6.2', }, }) // 渲染模板 generator.render('./template', {}) }
能夠看到渲染的代碼爲 generator.render('./template', {})
。./template
是模板目錄的路徑:
全部的模板代碼都放在 template
目錄下,vuex
將會在用戶建立的目錄下的 src
目錄生成 store
文件夾,裏面有一個 index.js
文件。它的內容爲:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { }, mutations: { }, actions: { }, modules: { }, })
這裏簡單描述一下 generator.render()
的渲染過程。
第一步, 使用 globby 讀取模板目錄下的全部文件:
const _files = await globby(['**/*'], { cwd: source, dot: true })
第二步,遍歷全部讀取的文件。若是文件是二進制文件,則不做處理,渲染時直接生成文件。不然讀取文件內容,再調用 ejs 進行渲染:
// 返回文件內容 const template = fs.readFileSync(name, 'utf-8') return ejs.render(template, data, ejsOptions)
使用 ejs
的好處,就是能夠結合變量來決定是否渲染某些代碼。例如 webpack
的模板中有這樣一段代碼:
module: { rules: [ <%_ if (hasBabel) { _%> { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, }, <%_ } _%> ], },
ejs
能夠根據用戶是否選擇了 babel
來決定是否渲染這段代碼。若是 hasBabel
爲 false
,則這段代碼:
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, },
將不會被渲染出來。hasBabel
的值是調用 render()
時用參數傳過去的:
generator.render('./template', { hasBabel: options.features.includes('babel'), lintOnSave: options.lintOn.includes('save'), })
第三步,注入特定代碼。回想一下剛纔 vuex
中的:
// 向入口文件 `src/main.js` 注入代碼 import store from './store' generator.injectImports(generator.entryFile, `import store from './store'`) // 向入口文件 `src/main.js` 的 new Vue() 注入選項 store generator.injectRootOptions(generator.entryFile, `store`)
這兩行代碼的做用是:在項目入口文件 src/main.js
中注入特定的代碼。
vuex
是 vue
的一個狀態管理庫,屬於 vue
全家桶中的一員。若是建立的項目沒有選擇 vuex
和 vue-router
。則 src/main.js
的代碼爲:
import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: (h) => h(App), }).$mount('#app')
若是選擇了 vuex
,它會注入上面所說的兩行代碼,如今 src/main.js
代碼變爲:
import Vue from 'vue' import store from './store' // 注入的代碼 import App from './App.vue' Vue.config.productionTip = false new Vue({ store, // 注入的代碼 render: (h) => h(App), }).$mount('#app')
這裏簡單描述一下代碼的注入過程:
package.json
的部分選項一些第三方庫的配置項能夠放在 package.json
文件,也能夠本身獨立生成一份文件。例如 babel
在 package.json
中注入的配置爲:
babel: { presets: ['@babel/preset-env'], }
咱們能夠調用 generator.extractConfigFiles()
將內容提取出來並生成 babel.config.js
文件:
module.exports = { presets: ['@babel/preset-env'], }
渲染好的模板文件和 package.json
文件目前仍是在內存中,並無真正的在硬盤上建立。這時能夠調用 writeFileTree()
將文件生成:
const fs = require('fs-extra') const path = require('path') module.exports = async function writeFileTree(dir, files) { Object.keys(files).forEach((name) => { const filePath = path.join(dir, name) fs.ensureDirSync(path.dirname(filePath)) fs.writeFileSync(filePath, files[name]) }) }
這段代碼的邏輯以下:
例如如今一個文件路徑爲 src/test.js
,第一次寫入時,因爲尚未 src
目錄。因此會先生成 src
目錄,再生成 test.js
文件。
webpack 須要提供開發環境下的熱加載、編譯等服務,還須要提供打包服務。目前 webpack 的代碼比較少,功能比較簡單。並且生成的項目中,webpack 配置代碼是暴露出來的。這留待 v3 版本再改進。
添加一個新功能,須要在兩個地方添加代碼:分別是 lib/promptModules
和 lib/generator
。在 lib/promptModules
中添加的是這個功能相關的交互提示語。在 lib/generator
中添加的是這個功能相關的依賴和模板代碼。
不過不是全部的功能都須要添加模板代碼的,例如 babel
就不須要。在添加新功能時,有可能會對已有的模板代碼形成影響。例如我如今須要項目支持 ts
。除了添加 ts
相關的依賴,還得在 webpack
vue
vue-router
vuex
linter
等功能中修改原有的模板代碼。
舉個例子,在 vue-router
中,若是支持 ts
,則這段代碼:
const routes = [ // ... ]
須要修改成:
<%_ if (hasTypeScript) { _%> const routes: Array<RouteConfig> = [ // ... ] <%_ } else { _%> const routes = [ // ... ] <%_ } _%>
由於 ts
的值有類型。
總之,添加的新功能越多,各個功能的模板代碼也會愈來愈多。而且還須要考慮到各個功能之間的影響。
下載依賴須要使用 execa,它能夠調用子進程執行命令。
const execa = require('execa') module.exports = function executeCommand(command, cwd) { return new Promise((resolve, reject) => { const child = execa(command, [], { cwd, stdio: ['inherit', 'pipe', 'inherit'], }) child.stdout.on('data', buffer => { process.stdout.write(buffer) }) child.on('close', code => { if (code !== 0) { reject(new Error(`command failed: ${command}`)) return } resolve() }) }) } // create.js 文件 console.log('\n正在下載依賴...\n') // 下載依賴 await executeCommand('npm install', path.join(process.cwd(), name)) console.log('\n依賴下載完成! 執行下列命令開始開發:\n') console.log(`cd ${name}`) console.log(`npm run dev`)
調用 executeCommand()
開始下載依賴,參數爲 npm install
和用戶建立的項目路徑。爲了能讓用戶看到下載依賴的過程,咱們須要使用下面的代碼將子進程的輸出傳給主進程,也就是輸出到控制檯:
child.stdout.on('data', buffer => { process.stdout.write(buffer) })
下面我用動圖演示一下 v1 版本的建立過程:
建立成功的項目截圖:
第二個版本在 v1 的基礎上添加了一些輔助功能:
建立項目時,先提早判斷一下該項目是否存在:
const targetDir = path.join(process.cwd(), name) // 若是目標目錄已存在,詢問是覆蓋仍是合併 if (fs.existsSync(targetDir)) { // 清空控制檯 clearConsole() 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' }, ], }, ]) if (action === 'overwrite') { console.log(`\nRemoving ${chalk.cyan(targetDir)}...`) await fs.remove(targetDir) } }
若是選擇 overwrite
,則進行移除 fs.remove(targetDir)
。
先在代碼中提早把默認配置的代碼寫好:
exports.defaultPreset = { features: ['babel', 'linter'], historyMode: false, eslintConfig: 'airbnb', lintOn: ['save'], }
這個配置默認使用 babel
和 eslint
。
而後生成交互提示語時,先調用 getDefaultPrompts()
方法獲取默認配置。
getDefaultPrompts() { const presets = this.getPresets() const presetChoices = Object.entries(presets).map(([name, preset]) => { let displayName = name return { name: `${displayName} (${preset.features})`, value: name, } }) const presetPrompt = { name: 'preset', type: 'list', message: `Please pick a preset:`, choices: [ // 默認配置 ...presetChoices, // 這是手動模式提示語 { name: 'Manually select features', value: '__manual__', }, ], } const featurePrompt = { name: 'features', when: isManualMode, type: 'checkbox', message: 'Check the features needed for your project:', choices: [], pageSize: 10, } return { presetPrompt, featurePrompt, } }
這樣配置後,在用戶選擇功能前會先彈出這樣的提示語:
在 vue-cli
建立項目時,會生成一個 .vuerc
文件,裏面會記錄一些關於項目的配置信息。例如使用哪一個包管理器、npm 源是否使用淘寶源等等。爲了不和 vue-cli
衝突,本腳手架生成的配置文件爲 .mvcrc
。
這個 .mvcrc
文件保存在用戶的 home
目錄下(不一樣操做系統目錄不一樣)。個人是 win10 操做系統,保存目錄爲 C:\Users\bin
。獲取用戶的 home
目錄能夠經過如下代碼獲取:
const os = require('os') os.homedir()
.mvcrc
文件還會保存用戶建立項目的配置,這樣當用戶從新建立項目時,就能夠直接選擇之前建立過的配置,不用再一步步的選擇項目功能。
在第一次建立項目時,.mvcrc
文件是不存在的。若是這時用戶還安裝了 yarn,腳手架就會提示用戶要使用哪一個包管理器:
// 讀取 `.mvcrc` 文件 const savedOptions = loadOptions() // 若是沒有指定包管理器而且存在 yarn if (!savedOptions.packageManager && hasYarn) { const packageManagerChoices = [] if (hasYarn()) { packageManagerChoices.push({ name: 'Use Yarn', value: 'yarn', short: 'Yarn', }) } packageManagerChoices.push({ name: 'Use NPM', value: 'npm', short: 'NPM', }) otherPrompts.push({ name: 'packageManager', type: 'list', message: 'Pick the package manager to use when installing dependencies:', choices: packageManagerChoices, }) }
當用戶選擇 yarn 後,下載依賴的命令就會變爲 yarn
;若是選擇了 npm,下載命令則爲 npm install
:
const PACKAGE_MANAGER_CONFIG = { npm: { install: ['install'], }, yarn: { install: [], }, } await executeCommand( this.bin, // 'yarn' or 'npm' [ ...PACKAGE_MANAGER_CONFIG[this.bin][command], ...(args || []), ], this.context, )
當用戶選擇了項目功能後,會先調用 shouldUseTaobao()
方法判斷是否須要切換淘寶源:
const execa = require('execa') const chalk = require('chalk') const request = require('./request') const { hasYarn } = require('./env') const inquirer = require('inquirer') const registries = require('./registries') const { loadOptions, saveOptions } = require('./options') async function ping(registry) { await request.get(`${registry}/vue-cli-version-marker/latest`) return registry } function removeSlash(url) { return url.replace(/\/$/, '') } let checked let result module.exports = async function shouldUseTaobao(command) { if (!command) { command = hasYarn() ? 'yarn' : 'npm' } // ensure this only gets called once. if (checked) return result checked = true // previously saved preference const saved = loadOptions().useTaobaoRegistry if (typeof saved === 'boolean') { return (result = saved) } const save = val => { result = val saveOptions({ useTaobaoRegistry: val }) return val } let userCurrent try { userCurrent = (await execa(command, ['config', 'get', 'registry'])).stdout } catch (registryError) { try { // Yarn 2 uses `npmRegistryServer` instead of `registry` userCurrent = (await execa(command, ['config', 'get', 'npmRegistryServer'])).stdout } catch (npmRegistryServerError) { return save(false) } } const defaultRegistry = registries[command] if (removeSlash(userCurrent) !== removeSlash(defaultRegistry)) { // user has configured custom registry, respect that return save(false) } let faster try { faster = await Promise.race([ ping(defaultRegistry), ping(registries.taobao), ]) } catch (e) { return save(false) } if (faster !== registries.taobao) { // default is already faster return save(false) } if (process.env.VUE_CLI_API_MODE) { return save(true) } // ask and save preference const { useTaobaoRegistry } = await inquirer.prompt([ { name: 'useTaobaoRegistry', type: 'confirm', message: chalk.yellow( ` Your connection to the default ${command} registry seems to be slow.\n` + ` Use ${chalk.cyan(registries.taobao)} for faster installation?`, ), }, ]) // 註冊淘寶源 if (useTaobaoRegistry) { await execa(command, ['config', 'set', 'registry', registries.taobao]) } return save(useTaobaoRegistry) }
上面代碼的邏輯爲:
.mvcrc
是否有 useTaobaoRegistry
選項。若是有,直接將結果返回,無需判斷。get
請求,經過 Promise.race()
來調用。這樣更快的那個請求會先返回,從而知道是默認源仍是淘寶源速度更快。await execa(command, ['config', 'set', 'registry', registries.taobao])
將當前 npm 的源改成淘寶源,即 npm config set registry https://registry.npm.taobao.org
。若是是 yarn,則命令爲 yarn config set registry https://registry.npm.taobao.org
。其實 vue-cli
是沒有這段代碼的:
// 註冊淘寶源 if (useTaobaoRegistry) { await execa(command, ['config', 'set', 'registry', registries.taobao]) }
這是我本身加的。主要是我沒有在 vue-cli
中找到顯式註冊淘寶源的代碼,它只是從配置文件讀取出是否使用淘寶源,或者將是否使用淘寶源這個選項寫入配置文件。另外 npm 的配置文件 .npmrc
是能夠更改默認源的,若是在 .npmrc
文件直接寫入淘寶的鏡像地址,那 npm 就會使用淘寶源下載依賴。但 npm 確定不會去讀取 .vuerc
的配置來決定是否使用淘寶源。
對於這一點我沒搞明白,因此在用戶選擇了淘寶源以後,手動調用命令註冊一遍。
若是用戶建立項目時選擇手動模式,在選擇完一系列功能後,會彈出下面的提示語:
詢問用戶是否將此次的項目選擇保存爲默認配置,若是用戶選擇是,則彈出下一個提示語:
讓用戶輸入保存配置的名稱。
這兩句提示語相關的代碼爲:
const otherPrompts = [ { 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:', }, ]
保存配置的代碼爲:
exports.saveOptions = (toSave) => { const options = Object.assign(cloneDeep(exports.loadOptions()), toSave) for (const key in options) { if (!(key in exports.defaults)) { delete options[key] } } 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})`, ) } } exports.savePreset = (name, preset) => { const presets = cloneDeep(exports.loadOptions().presets || {}) presets[name] = preset return exports.saveOptions({ presets }) }
以上代碼直接將用戶的配置保存到 .mvcrc
文件中。下面是我電腦上的 .mvcrc
的內容:
{ "packageManager": "npm", "presets": { "test": { "features": [ "babel", "linter" ], "eslintConfig": "airbnb", "lintOn": [ "save" ] }, "demo": { "features": [ "babel", "linter" ], "eslintConfig": "airbnb", "lintOn": [ "save" ] } }, "useTaobaoRegistry": true }
下次再建立項目時,腳手架就會先讀取這個配置文件的內容,讓用戶決定是否使用已有的配置來建立項目。
至此,v2 版本的內容就介紹完了。
因爲 vue-cli
關於插件的源碼我尚未看完,因此這篇文章只講解前兩個版本的源碼。v3 版本等我看完 vue-cli
的源碼再回來填坑,預計在 3 月初就能夠完成。
若是你想了解更多關於前端工程化的文章,能夠看一下我寫的《帶你入門前端工程》。 這裏是全文目錄: