本系列一共6篇文章
- Vue CLI 源码探索 [开篇] 整体介绍下 Vue CLI
- Vue CLI 源码探索 [一] @vue/cli 包的概览,已经第一个命令 vue create
- Vue CLI 源码探索 [二] 涉及三个命令(vue add/invoke/inspect)
- Vue CLI 源码探索 [三] 和想象中不太一样的 vue serve/build
- Vue CLI 源码探索 [四] 其他命令(vue init/outdated/upgrade/migrate/info//--help)
- Vue CLI 源码探索 [五] 分析下 Vue CLI 中测试相关的内容
- Vue CLI 源码探索 [六] 探索下 Vue CLI 的插件机制,内容较多,请慢慢看。涉及如下插件(@vue/cli-plugin-vuex/router/babel/typescript/eslint)
下面正文开始啦 ^_^
vue init template app-name
todo,暂时忽略该命令。一个是因为调试过程中出现报错,第二个是根据我的理解,以后该命令会被废弃。
调试配置
launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
// "program": "${workspaceFolder}/packages/@vue/cli-service/bin/vue-cli-service",
"program": "${workspaceFolder}/packages/@vue/cli/bin/vue",
"args": [
"init",
"simple-webapck",
"my-project"
]
}
]
}
调试报错
vue-cli · Failed to download repo vuejs-templates/simple-webapck: Response code 404 (Not Found)
vue config [value]
官方介绍:
有些针对 @vue/cli 的全局配置,例如你惯用的包管理器和你本地保存的 preset,都保存在 home 目录下一个名叫 .vuerc 的 JSON 文件。你可以用编辑器直接编辑这个文件来更改已保存的选项。
你也可以使用 vue config 命令来审查或修改全局的 CLI 配置。
该命令是用来获取或者设置某个配置,这里的配置指的是 /Users/xxxx/.vuerc 这个文件的的配置
看下我当前的配置,记录了预设等等
{
"useTaobaoRegistry": false,
"latestVersion": "4.2.3",
"lastChecked": 1583721075145,
"packageManager": "yarn",
"presets": {
"llccing-default": {
"useConfigFiles": true,
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-router": {
"historyMode": false
},
"@vue/cli-plugin-eslint": {
"config": "base",
"lintOn": [
"save"
]
}
},
"cssPreprocessor": "stylus"
}
}
}
用处
这个config的作用是针对当前计算机中所有项目的通用的config,也就是说是跨项目的。实际开发过程中,感觉使用场景还是比较少。
当然我们现在公司的处理方式是用了远程 preset 的方式,然后统一管理这个preset,达到组内所有人都公用一个配置的目的。
所以我觉得在企业开发中,.vuerc 中的配置用处还是比较小。
调试配置
{
// 保留主要部分,其他和前面一致
"program": "${workspaceFolder}/packages/@vucli/bin/vue",
"args": [
"config",
]
}
源码探索
主要的代码逻辑都在 @vue/cli/lib/config.js 中,也很清晰
- 首先读取
.vuerc文件 - 如果
vue config后面没有其他参数,则打印当前的.vuerc文件内容 - 然后根据参数类型:
get/delete/edit/set分别操作对应的值
vue config edit
在执行这个命令的时候,作者还针对编辑器编辑 .vuerc 文件单独抽象了一个库launch-editor出来,合理的抽象,确实能够降低复杂度,且代码职责解耦,利于维护。
解决 edit命令报错的问题:将code命令加入 PATH 中
有意思的地方
// 看了一下这个 os 是node.js内置模块,提供了操作系统相关的使用方法和属性
// homedir 返回当前用户的胡目录的字符串格式路径
const homedir = require('os').homedir()
下面再看一段代码,这段代码来自 @vue/cli-shared-utils/lib/object.js,也就是工具库中对象操作的方法,这个方法厉害之处是:如果你想给取得 const obj = {a: {b: {c: { d: 123123, e: '我是eee' } } } },这个对象中 d 的值,只要执行get(obj, 'a.b.c.d') 即可。
假设这样调用get(obj, 'a.b.c.d'),下面分析下逻辑:
exports.get = function (target, path) {
// fields = ['a', 'b', 'c', 'd']
const fields = path.split('.')
// obj = {a: {b: {c: { d: 123123, e: '我是eee' } } } }
let obj = target
// l = 4
const l = fields.length
// 通过循环,逐层深入,这里i最大是2
for (let i = 0; i < l - 1; i++) {
const key = fields[i]
if (!obj[key]) {
return undefined
}
obj = obj[key]
}
// obj = { d: 123123, e: '我是eee' }
// fields[l - 1] = d
// 所以 obj[fields[l - 1]] = obj[d] = 123123
return obj[fields[l - 1]]
}
这个写法在vue.js的源码中也能够看到。
vue outdated
'(experimental) check for outdated vue cli service / plugins'
上面是代码中的功能描述原文,我理解这个是说:实验性的功能,用来检查 服务(@vue/cli-service) 或者 插件(官方插件@vue/cli-plugin-* 自定义插件vue-cli-plugin-*) 是否过期
调试配置
{
"program": "${workspaceFolder}/packages/@vue/cli/bin/vue",
"args": [
"outdated"
]
}
上面的这个调试配置是能够正常执行的,但是有一点缺憾的是,我现在还没有发现如何能能够在某个项目中执行 vue outdated 命令,然后能够在当前的 vue-cli 项目中打断点的方式。
所以其中的某些代码逻辑就不能实际执行到,当然这个对我们查看代码会有影响,但是没那么大。
源码探索
命令注册
program
.command('outdated')
.description('(experimental) check for outdated vue cli service / plugins')
.option('--next', 'Also check for alpha / beta / rc versions when upgrading')
.action((cmd) => {
require('../lib/outdated')(cleanArgs(cmd))
})
主要逻辑
@vue/cli/lib/outdated.js 这个文件中也很清晰,加载 ./Upgrader 类,实例化,调用 checkForUpdates 方法。
const Upgrader = require('./Upgrader')
async function outdated (options, context = process.cwd()) {
const upgrader = new Upgrader(context)
return upgrader.checkForUpdates(options.next)
}
首先 Upgrader 实例化时,构造函数中初始了两个属性,并且又实例化了 PackageManager 类。
this.pkg = getPkg(this.context)
this.pm = new PackageManager({ context })
这个 PackageManager 类我们可以通过他的方法来大致了解他的作用
class PackageManager {
async runCommand (command, args) {...}
async install () {
...
return await this.runCommand('install')
}
async add () {
...
return await this.runCommand('add', [packageName, ...args])
}
async remove (packageName) {
return await this.runCommand('remove', [packageName])
}
async upgrade (packageName) {
...
return await this.runCommand('add', [packageName])
}
}
主要是针对依赖包的处理,安装、添加、移除、升级,版本比较等。
下面看下这个方法,因为要做版本比较,所以需要有比较的源 -- 也就是最新版本。getRemoteVersion 这个方法是用来获取远程版本的
async getRemoteVersion (packageName, versionRange = 'latest') {
const metadata = await this.getMetadata(packageName)
if (Object.keys(metadata['dist-tags']).includes(versionRange)) {
// 这里判断如果版本匹配,则和就直接返回版本号
return metadata['dist-tags'][versionRange]
}
const versions = Array.isArray(metadata.versions) ? metadata.versions : Object.keys(metadata.versions)
// 然后这里返回所有匹配的版本中,最大的版本。
return semver.maxSatisfying(versions, versionRange)
}
其中的 this.getMetadata(packageName) 这个方法的内容可以看下,
const url = `${registry.replace(/\/$/g, '')}/${packageName}`
try {
// 请求url,url类似这样 http://npm.intra.xiaojukeji.com/@vue/cli
metadata = (await request.get(url, { headers })).body
metadataCache.set(metadataKey, metadata)
return metadata
} catch (e) {
error(`Failed to get response from ${url}`)
throw e
}
我通过curl的方式模仿request请求,curl http://npm.intra.xiaojukeji.com/@vue/cli 拿到的返回结果如下
"_id" : "@vue/cli",
"_rev" : "45453609",
"name" : "@vue/cli",
"description" : "Command line interface for rapid Vue.js development",
"dist-tags" : {
"next" : "4.1.0",
"latest" : "4.2.3"
},
"versions" : {
"4.1.1" : {
"name" : "@vue/cli",
"description" : "Command line interface for rapid Vue.js development",
"version" : "4.1.1",
"author" : {
"name" : "Evan You"
},
},
"4.1.2": {},
...
}
接下来再看 checkForUpdates 方法,首先是获取当前项目依赖中的 if (name !== '@vue/cli-service' && !isPlugin(name)) 主要是找服务和插件,其中 isPlugin 这个方法尤其值得好好品一下:
const pluginRE = /^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/
exports.isPlugin = id => pluginRE.test(id)
看到这个正则我只能空叹:“厉害!”,当然我们大致可以理解是匹配了名字来判断是否为插件。
获取到需要过期的插件后,通过控制台输出,从而提示用户进行升级。
vue upgrade [plugin-name]
引用原文:(experimental) upgrade vue cli service / plugins
这个是用来升级 cli-service 和 plugins 的。
源码探索
调试脚本
{
"program": "${workspaceFolder}/packages/@vue/cli/bin/vue",
"args": [
"upgrade",
"@vue/cli-service"
]
}
命令注册
program
.command('upgrade [plugin-name]')
// 这也是个实验性的功能
.description('(experimental) upgrade vue cli service / plugins')
.option('-t, --to <version>', 'Upgrade <package-name> to a version that is not latest')
.option('-f, --from <version>', 'Skip probing installed plugin, assuming it is upgraded from the designated version')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies')
.option('--all', 'Upgrade all plugins')
.option('--next', 'Also check for alpha / beta / rc versions when upgrading')
.action((packageName, cmd) => {
require('../lib/upgrade')(packageName, cleanArgs(cmd))
})
代码的主要逻辑在 @vue/cli/lib/upgrade.js 这个文件中
async function upgrade (packageName, options, context = process.cwd()) {
// 首先校验了项目目录下是否有代码没有提交
if (!(await confirmIfGitDirty(context))) {
return
}
// 然后实例化了 `Upgrader` 类
const upgrader = new Upgrader(context)
// 如果没指定具体的升级包
if (!packageName) {
if (options.to) {
error(`Must specify a package name to upgrade to ${options.to}`)
process.exit(1)
}
if (options.all) {
return upgrader.upgradeAll(options.next)
}
// 通过 `const upgradable = await upgrader.checkForUpdates(options.next)` 检查是否有更新,
const upgradable = await upgrader.checkForUpdates(options.next)
if (upgradable) {
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: 'Continue to upgrade these plugins?',
default: true
}
])
if (ok) {
return upgrader.upgradeAll(options.next)
}
}
return
}
// 如果指定了升级的包,那么直接升级就好
return upgrader.upgrade(packageName, options)
}
通过 git status --porcelain 命令检查当前仓库中是否有未提交的内容(也就是看git的工作区和暂存区是否有新的内容)
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: context })
if (!stdout) {
return true
}
@vue/cli/lib/Upgrader.js 这里我们看下 Upgrader 类的 upgrade 方法:
async upgrade (pluginId, options) {
...
// 这里是说如果要升级的包存在 migrator(升级器)则调用它
const pluginMigrator = loadModule(`${packageName}/migrator`, this.context) || noop
await runMigrator(
this.context,
{
id: packageName,
apply: pluginMigrator,
baseVersion: installed
},
this.pkg
)
}
runMigrator 来自 @vue/cli/lib/migrate.js
async function runMigrator (context, plugin, pkg = getPkg(context)) {
const afterInvokeCbs = []
// 这里实例化了迁移器
const migrator = new Migrator(context, {
plugin,
pkg,
files: await readFiles(context),
afterInvokeCbs
})
await migrator.generate({
extractConfigFiles: true,
checkExisting: true
})
}
看下 Migrator 的真面目,@vue/cli/lib/Migrator.js:
// 这里 Migrator 继承了 Generator,所以可以使用 Generator 的方法
module.exports = class Migrator extends Generator {
...
// 这里是对原有方法的覆盖
async generate (...args) {
const plugin = this.migratorPlugin
// apply migrators from plugins
// 调用插件的 migrator
const api = new MigratorAPI(
plugin.id,
plugin.baseVersion,
this,
plugin.options,
this.rootOptions
)
// 这里是调用了 插件中的 migrator 方法
await plugin.apply(api, plugin.options, this.rootOptions, this.invoking)
// 这里仍然调用父类的方法
await super.generate(...args)
}
}
我们看上面实例化了 MigratorAPI,我们看下他的本来面目 @vue/cli/lib/MigratorAPI :
class MigratorAPI extends GeneratorAPI {
/**
* @param {string} id - Id of the owner plugin
* @param {Migrator} migrator - The invoking Migrator instance
* @param {object} options - options passed to this plugin
* @param {object} rootOptions - root options (the entire preset)
*/
constructor (id, baseVersion, migrator, options, rootOptions) {
super(id, migrator, options, rootOptions)
this.baseVersion = baseVersion
this.migrator = this.generator
}
fromVersion (range) {
return semver.satisfies(this.baseVersion, range)
}
}
MigratorAPI 继承了 GeneratorAPI,所以她就拥有了 GeneratorAPI 所有的方法
vue migrate [plugin-name]
(experimental) run migrator for an already-installed cli plugin
实验的功能,用于迁移已经安装的插件
源码探索
命令注册
program
.command('migrate [plugin-name]')
.description('(experimental) run migrator for an already-installed cli plugin')
// TODO: use `requiredOption` after upgrading to commander 4.x
.option('-f, --from <version>', 'The base version for the migrator to migrate from')
.action((packageName, cmd) => {
require('../lib/migrate')(packageName, cleanArgs(cmd))
})
调试配置
{
"program": "${workspaceFolder}/packages/@vue/cli/bin/vue",
"args": [
"migrate",
"@vue/cli-service",
"--from=4.2.1"
]
}
逻辑
主要的逻辑从 @vue/cli/lib/migrate.js 开始
async function migrate (pluginId, { from }, context = process.cwd()) {
// TODO: remove this after upgrading to commander 4.x
// 这里会强制设置迁移的基础版本
if (!from) {
throw new Error(`Required option 'from' not specified`)
}
const pluginName = resolvePluginId(pluginId)
// 这里是载入了插件的 migrator 文件,留给插件开发者迁移入口,具体的迁移逻辑有插件开发者实现。
const pluginMigrator = loadModule(`${pluginName}/migrator`, context)
if (!pluginMigrator) {
log(`There's no migrator in ${pluginName}`)
return
}
await runMigrator(context, {
id: pluginName,
apply: pluginMigrator,
baseVersion: from
})
}
再看下 runMigrator 方法,实例化了 Migrator 类,而 Migtrator 类则继承了 Generator 类并且重写了 generate 方法
async generate (...args) {
const plugin = this.migratorPlugin
// apply migrators from plugins
const api = new MigratorAPI(
plugin.id,
plugin.baseVersion,
this,
plugin.options,
this.rootOptions
)
await plugin.apply(api, plugin.options, this.rootOptions, this.invoking)
await super.generate(...args)
}
其中 MigratorAPI 是继承了 GeneratorAPI。
TODO 这一段的代码是的设计是很精巧的,Generator/GeneratorAPI/Migrator/MigratorAPI,并且 Migrator 继承 Generator,MigratorAPI 继承 GeneratorAPI,总觉这里是应用了一种或者多多种设计模式,但是现在还没想清晰,先留个疑问在此。
我们再看 runMigrator 的后续逻辑,执行 migrator 实例的 generate 方法,同时如果有新的依赖则通过 pm(PackageManager 实例)安装,如果有钩子函数则执行钩子函数,然后通过 git ls-files -o --exclude-standard --full-name 命令识别出本次迁移变化的内容,提醒用户关注这些文件变化。
vue info
print debugging information about your environment
打印当前环境的调试信息
源码探索
program
.command('info')
.description('print debugging information about your environment')
.action((cmd) => {
console.log(chalk.bold('\nEnvironment Info:'))
require('envinfo').run(
{
System: ['OS', 'CPU'],
Binaries: ['Node', 'Yarn', 'npm'],
Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'],
npmPackages: '/**/{typescript,*vue*,@vue/*/}',
npmGlobalPackages: ['@vue/cli']
},
{
showNotFound: true,
duplicates: true,
fullTree: true
}
).then(console.log)
})
这里主要是依赖了 envinfo 的能力。
这里看一下我当前环境的信息,只所以 @vue/cli: Not Found 这个是因为我当前是调试模式,在@vue/cli目录执行了 yarn link,所以这里的 @vue/cli 是 Not Found
Environment Info:
System:
OS: macOS Sierra 10.12.6
CPU: (4) x64 Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
Binaries:
Node: 12.13.0 - ~/.nvm/versions/node/v12.13.0/bin/node
Yarn: 1.22.0 - ~/.yarn/bin/yarn
npm: 6.12.0 - ~/.nvm/versions/node/v12.13.0/bin/npm
Browsers:
Chrome: 80.0.3987.149
Firefox: 72.0.1
Safari: 12.1.2
npmPackages:
@vue/babel-helper-vue-jsx-merge-props: 1.0.0
@vue/babel-plugin-transform-vue-jsx: 1.0.0
@vue/babel-preset-app: 3.11.0
@vue/babel-preset-jsx: 1.1.0
@vue/babel-sugar-functional-vue: 1.0.0
@vue/babel-sugar-inject-h: 1.0.0
@vue/babel-sugar-v-model: 1.0.0
@vue/babel-sugar-v-on: 1.1.0
@vue/component-compiler-utils: 3.0.0
vue: 2.6.10
vue-hot-reload-api: 2.3.4
vue-loader: 15.7.1
vue-router: 3.1.3
vue-server-renderer: 2.6.10
vue-style-loader: 4.1.2
vue-template-compiler: 2.6.10
vue-template-es2015-compiler: 1.9.1
vuepress: ^1.1.0 => 1.1.0
vuepress-html-webpack-plugin: 3.2.0
vuepress-plugin-container: 2.0.2
npmGlobalPackages:
@vue/cli: Not Found
vue <command>
output help information on unknown commands
主要是针对没有设置的命令,输出帮助信息
program
.arguments('<command>')
.action((cmd) => {
program.outputHelp()
console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
console.log()
// 这个建议命令是个人性化的功能,如果你不小心输错一个字符,他会提示你正确的
suggestCommands(cmd)
})
我们看下 suggestCommands 的代码
function suggestCommands (unknownCommand) {
// 取得所有注册命令
const availableCommands = program.commands.map(cmd => cmd._name)
let suggestion
availableCommands.forEach(cmd => {
/*
举个拼配的例子
实际输入值 bast
'' leven 4
计划输入值 best leven 1
这样的情况下,isBestMatch = true
*/
const isBestMatch = leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand)
// 如果拼配不上的字符数 < 3 并且 isBestMatch=true 则说明找到了 “建议命令”
if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
suggestion = cmd
}
})
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`))
}
}
leven README中作者这样描述:测量两个不同的字符串的差别,实现莱温斯坦距离算法最快的js实现,没有之一...,说明很有信心的!
vue --help
help 主要是为了输出帮助信息
// add some useful info on help
program.on('--help', () => {
console.log()
console.log(` Run ${chalk.cyan(`vue <command> --help`)} for detailed usage of given command.`)
console.log()
})
// 这个循环是给每一个命令都自定义了帮助信息
program.commands.forEach(c => c.on('--help', () => console.log()))
至此,@vue/cli/bin 中涉及的15个命令就都过了一遍,相信你和我一样都有收获。
感谢阅读
感谢你阅读到这里,翻译的不好的地方,还请指点。希望我的内容能让你受用,再次感谢。by llccing 千里