第二个版本 v2
第二个版本在 v1 的基础上添加了一些辅助功能:
- 创建项目时判断该项目是否已存在,支持覆盖和合并创建。
- 选择功能时提供默认配置和手动选择两种模式。
- 如果用户的环境同时存在 yarn 和 npm,则会提示用户要使用哪个包管理器。
- 如果 npm 的默认源速度比较慢,则提示用户是否要切换到淘宝源。
- 如果用户是手动选择功能,在结束后会询问用户是否要将这次的选择保存为默认配置。
覆盖和合并
创建项目时,先提前判断一下该项目是否存在:
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, )
切换 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) }
上面代码的逻辑为:
- 先判断默认配置文件
.mvcrc
是否有useTaobaoRegistry
选项。如果有,直接将结果返回,无需判断。 - 向 npm 默认源和淘宝源各发一个
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 月初就可以完成。
如果你想了解更多关于前端工程化的文章,可以看一下我写的《带你入门前端工程》。 这里是全文目录:
- 技术选型:如何进行技术选型?
- 统一规范:如何制订规范并利用工具保证规范被严格执行?
- 前端组件化:什么是模块化、组件化?
- 测试:如何写单元测试和 E2E(端到端) 测试?
- 构建工具:构建工具有哪些?都有哪些功能和优势?
- 自动化部署:如何利用 Jenkins、Github Actions 自动化部署项目?
- 前端监控:讲解前端监控原理及如何利用 sentry 对项目实行监控。
- 性能优化(一):如何检测网站性能?有哪些实用的性能优化规则?
- 性能优化(二):如何检测网站性能?有哪些实用的性能优化规则?
- 重构:为什么做重构?重构有哪些手法?
- 微服务:微服务是什么?如何搭建微服务项目?
- Severless:Severless 是什么?如何使用 Severless?