手把手教你写一个脚手架(下)

简介: 手把手教你写一个脚手架(下)

第二个版本 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?

参考资料

目录
相关文章
|
前端开发 JavaScript 开发工具
手把手教你写一个脚手架(上)
手把手教你写一个脚手架
155 2
|
前端开发 JavaScript 测试技术
手把手带你入门前端工程化——超详细教程(一)
手把手带你入门前端工程化——超详细教程
316 0
|
5月前
|
JavaScript 前端开发 编译器
Vue快速上手笔记2 - 开发环境的搭建
Vue快速上手笔记2 - 开发环境的搭建
44 0
|
JavaScript
手把手教你写一个脚手架(中)
手把手教你写一个脚手架(中)
90 0
|
前端开发 Shell 项目管理
手把手教你写一个脚手架(二)
手把手教你写一个脚手架(二)
53 0
|
监控 前端开发 JavaScript
手把手带你入门前端工程化——超详细教程(三)
手把手带你入门前端工程化——超详细教程(三)
103 0
|
Web App开发 监控 前端开发
手把手带你入门前端工程化——超详细教程(四)
手把手带你入门前端工程化——超详细教程(四)
135 0
|
监控 前端开发 测试技术
手把手带你入门前端工程化——超详细教程(二)
手把手带你入门前端工程化——超详细教程(二)
81 0
|
JavaScript
vue项目创建手把手教会绝不迷路(赞赞赞)
vue项目创建手把手教会绝不迷路(赞赞赞)
48 0
|
前端开发 开发工具 git
前端脚手架的搭建
前端脚手架的搭建
174 0