<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        手把手教你寫一個腳手架

        譚光志

        最近在學習 vue-cli 的源碼,獲益良多。為了讓自己理解得更加深刻,我決定模仿它造一個輪子,爭取盡可能多的實現原有的功能。

        我將這個輪子分成三個版本:

        1. 盡可能用最少的代碼實現一個最簡版本的腳手架。
        2. 在 1 的基礎上添加一些輔助功能,例如選擇包管理器、npm 源等等。
        3. 實現插件化,可以自由的進行擴展。在不影響內部源碼的情況下,添加功能。

        有人可能不懂腳手架是什么。按我的理解,腳手架就是幫助你把項目的基礎架子搭好。例如項目依賴、模板、構建工具等等。讓你不用從零開始配置一個項目,盡可能快的進行業務開發。

        建議在閱讀本文時,能夠結合項目源碼一起配合使用,效果更好。這是項目地址 mini-cli。項目中的每一個分支都對應一個版本,例如第一個版本對應的 git 分支為 v1。所以在閱讀源碼時,記得要切換到對應的分支。

        第一個版本 v1

        第一個版本的功能比較簡單,大致為:

        1. 用戶輸入命令,準備創建項目。
        2. 腳手架解析用戶命令,并彈出交互語句,詢問用戶創建項目需要哪些功能。
        3. 用戶選擇自己需要的功能。
        4. 腳手架根據用戶的選擇創建 package.json 文件,并添加對應的依賴項。
        5. 腳手架根據用戶的選擇渲染項目模板,生成文件(例如 index.html、main.js、App.vue 等文件)。
        6. 執行 npm install 命令安裝依賴。

        項目目錄樹:

        ├─.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。這個庫的功能就是解析用戶的命令,提取出用戶的輸入交給腳手架。例如這段代碼:

        #!/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 文件添加這段代碼:

        "bin": {
          "mvc": "./bin/mvc.js"
        },

        再執行 npm link,就可以將 mvc 注冊成全局命令。這樣在電腦上的任何地方都能使用 mvc 命令了。實際上,就是用 mvc 命令來代替執行 node ./bin/mvc.js。

        假設用戶在命令行上輸入 mvc create demo(實際上執行的是 node ./bin/mvc.js create demo),commander 解析到命令 create 和參數 demo。然后腳手架可以在 action 回調里取到參數 name(值為 demo)。

        和用戶交互

        取到用戶要創建的項目名稱 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-router
        • vuex
        • babel
        • webpack
        • linter(eslint)

        由于這是一個 vue 相關的腳手架,所以 vue 是默認提供的,不需要用戶選擇。另外構建工具 webpack 提供了開發環境和打包的功能,也是必需的,不用用戶進行選擇。所以可供用戶選擇的功能只有 4 個:

        • vue-router
        • vuex
        • babel
        • linter

        現在我們先來看一下這 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)
            }
        }

        以上代碼的邏輯如下:

        1. 創建 creator 對象
        2. 調用 getPromptModules() 獲取所有功能的交互提示語
        3. 再調用 PromptModuleAPI 將所有交互提示語注入到 creator 對象
        4. 通過 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 模板

        每個模板的功能都差不多:

        1. pkg 變量注入依賴項
        2. 提供模板文件

        注入依賴

        下面是 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 來決定是否渲染這段代碼。如果 hasBabelfalse,則這段代碼:

        {
            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 中注入特定的代碼。

        vuexvue 的一個狀態管理庫,屬于 vue 全家桶中的一員。如果創建的項目沒有選擇 vuexvue-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')

        這里簡單描述一下代碼的注入過程:

        1. 使用 vue-codemod 將代碼解析成語法抽象樹 AST。
        2. 然后將要插入的代碼變成 AST 節點插入到上面所說的 AST 中。
        3. 最后將新的 AST 重新渲染成代碼。

        提取 package.json 的部分選項

        一些第三方庫的配置項可以放在 package.json 文件,也可以自己獨立生成一份文件。例如 babelpackage.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])
            })
        }

        這段代碼的邏輯如下:

        1. 遍歷所有渲染好的文件,逐一生成。
        2. 在生成一個文件時,確認它的父目錄在不在,如果不在,就先生成父目錄。
        3. 寫入文件。

        例如現在一個文件路徑為 src/test.js,第一次寫入時,由于還沒有 src 目錄。所以會先生成 src 目錄,再生成 test.js 文件。

        webpack

        webpack 需要提供開發環境下的熱加載、編譯等服務,還需要提供打包服務。目前 webpack 的代碼比較少,功能比較簡單。而且生成的項目中,webpack 配置代碼是暴露出來的。這留待 v3 版本再改進。

        添加新功能

        添加一個新功能,需要在兩個地方添加代碼:分別是 lib/promptModuleslib/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 版本的創建過程:

        創建成功的項目截圖:

        第二個版本 v2

        第二個版本在 v1 的基礎上添加了一些輔助功能:

        1. 創建項目時判斷該項目是否已存在,支持覆蓋和合并創建。
        2. 選擇功能時提供默認配置和手動選擇兩種模式。
        3. 如果用戶的環境同時存在 yarn 和 npm,則會提示用戶要使用哪個包管理器。
        4. 如果 npm 的默認源速度比較慢,則提示用戶是否要切換到淘寶源。
        5. 如果用戶是手動選擇功能,在結束后會詢問用戶是否要將這次的選擇保存為默認配置。

        覆蓋和合并

        創建項目時,先提前判斷一下該項目是否存在:

        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'],
        }

        這個配置默認使用 babeleslint。

        然后生成交互提示語時,先調用 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 目錄可以通過以下代碼獲?。?/p>

        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,
        )

        切換 npm 源

        當用戶選擇了項目功能后,會先調用 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)
        }

        上面代碼的邏輯為:

        1. 先判斷默認配置文件 .mvcrc 是否有 useTaobaoRegistry 選項。如果有,直接將結果返回,無需判斷。
        2. 向 npm 默認源和淘寶源各發一個 get 請求,通過 Promise.race() 來調用。這樣更快的那個請求會先返回,從而知道是默認源還是淘寶源速度更快。
        3. 如果淘寶源速度更快,向用戶提示是否切換到淘寶源。
        4. 如果用戶選擇淘寶源,則調用 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 月初就可以完成。

        如果你想了解更多關于前端工程化的文章,可以看一下我寫的《帶你入門前端工程》。 這里是全文目錄:

        1. 技術選型:如何進行技術選型?
        2. 統一規范:如何制訂規范并利用工具保證規范被嚴格執行?
        3. 前端組件化:什么是模塊化、組件化?
        4. 測試:如何寫單元測試和 E2E(端到端) 測試?
        5. 構建工具:構建工具有哪些?都有哪些功能和優勢?
        6. 自動化部署:如何利用 Jenkins、Github Actions 自動化部署項目?
        7. 前端監控:講解前端監控原理及如何利用 sentry 對項目實行監控。
        8. 性能優化(一):如何檢測網站性能?有哪些實用的性能優化規則?
        9. 性能優化(二):如何檢測網站性能?有哪些實用的性能優化規則?
        10. 重構:為什么做重構?重構有哪些手法?
        11. 微服務:微服務是什么?如何搭建微服務項目?
        12. Severless:Severless 是什么?如何使用 Severless?

        參考資料

        閱讀 3.5k
        5.6k 聲望
        10.3k 粉絲
        0 條評論
        5.6k 聲望
        10.3k 粉絲
        宣傳欄
        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕