关于javascript:手把手教你写一个脚手架

7次阅读

共计 17024 个字符,预计需要花费 43 分钟才能阅读完成。

最近在学习 vue-cli 的源码,获益良多。为了让本人了解得更加粗浅,我决定模拟它造一个轮子,争取尽可能多的实现原有的性能。

我将这个轮子分成三个版本:

  1. 尽可能用起码的代码实现一个最简版本的脚手架。
  2. 在 1 的根底上增加一些辅助性能,例如抉择包管理器、npm 源等等。
  3. 实现插件化,能够自在的进行扩大。在不影响外部源码的状况下,增加性能。

有人可能不懂脚手架是什么。按我的了解,脚手架就是帮忙你把我的项目的根底架子搭好。例如我的项目依赖、模板、构建工具等等。让你不必从零开始配置一个我的项目,尽可能快的进行业务开发。

倡议在浏览本文时,可能联合我的项目源码一起配合应用,成果更好。这是我的项目地址 mini-cli。我的项目中的每一个分支都对应一个版本,例如第一个版本对应的 git 分支为 v1。所以在浏览源码时,记得要切换到对应的分支。

第一个版本 v1

第一个版本的性能比较简单,大抵为:

  1. 用户输出命令,筹备创立我的项目。
  2. 脚手架解析用户命令,并弹出交互语句,询问用户创立我的项目须要哪些性能。
  3. 用户抉择本人须要的性能。
  4. 脚手架依据用户的抉择创立 package.json 文件,并增加对应的依赖项。
  5. 脚手架依据用户的抉择渲染我的项目模板,生成文件(例如 index.htmlmain.jsApp.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 目录能够通过以下代码获取:

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?

参考资料

  • vue-cli
正文完
 0