乐趣区

vue-cli3.0源码分析@vue/cli—–create

本文主要学习 vue-cli3.0 的源码的记录。源码地址: https://github.com/vuejs/vue-cli
主要对 packages 里面的 @vue 进行学习。如下图
在图中我们可以看到 vue-cli 中,不仅仅有初始化工程,还有许多通用的工具、插件。接下来我们就对这些插件进行学习。
首先我们来看 cli 的目录:首先来看 package.json
{
“name”: “@vue/cli”, // 名称
“version”: “3.5.5”, // 版本号
“bin”: {
“vue”: “bin/vue.js”
}, // 这个是用于命令窗口执行的命令;如果是全局安装了,那么 vue 就是一个命令值 vue xxxx
“engines”: {
“node”: “>=8.9”
} // 需要的 node 版本号
}
我们现在我们可以去看 bin/vue.js 文件, 对该文件给出注释,方便阅读
#!/usr/bin/env node
// 这边备注是 node 来解析, 当然如果不写也没事
// Check node version before requiring/doing anything else
// The user may be on a very old node version
const chalk = require(‘chalk’) // 用于输出有色彩
const semver = require(‘semver’) // 用于比较版本号
const requiredVersion = require(‘../package.json’).engines.node // 获取 node 版本号要求
// 检测 node 的版本号,如果不符合要求就给提示
function checkNodeVersion (wanted, id) {
if (!semver.satisfies(process.version, wanted)) {// process.version 表示当前 node 版本
console.log(chalk.red(
‘You are using Node ‘ + process.version + ‘, but this version of ‘ + id +
‘ requires Node ‘ + wanted + ‘.\nPlease upgrade your Node version.’
)) // 给出当前 vue-cli 需要的版本为多少
process.exit(1)
}
}

checkNodeVersion(requiredVersion, ‘vue-cli’)

if (semver.satisfies(process.version, ‘9.x’)) {
console.log(chalk.red(
`You are using Node ${process.version}.\n` +
`Node.js 9.x has already reached end-of-life and will not be supported in future major releases.\n` +
`It’s strongly recommended to use an active LTS version instead.`
))
} // 如果 node 为 9.x 那么给出相应的提示

const fs = require(‘fs’) // 文件
const path = require(‘path’) // 路径
const slash = require(‘slash’) // 用于转换 Windows 反斜杠路径转换为正斜杠路径 \ => /
const minimist = require(‘minimist’) // 用来解析从参数

// enter debug mode when creating test repo
if (
slash(process.cwd()).indexOf(‘/packages/test’) > 0 && (// process.cwd() 为当前绝对路径, 如 F:\packages\@vue\cli\bin
fs.existsSync(path.resolve(process.cwd(), ‘../@vue’)) ||
fs.existsSync(path.resolve(process.cwd(), ‘../../@vue’))
)
) {
process.env.VUE_CLI_DEBUG = true
}

const program = require(‘commander’) // node 对话,输入
const loadCommand = require(‘../lib/util/loadCommand’) // 用于查找模块

program
.version(require(‘../package’).version)
.usage(‘<command> [options]’)
上述是一些检测的代码。之后就要开始交互式的命令了。
1.create 入口
我们可以看到 program.command 就是创建的一个命令,后面会有很多的命令 create,add,invoke 等等,这一节主要来讲解 create
program
.command(‘create <app-name>’)
.description(‘create a new project powered by vue-cli-service’)
.option(‘-p, –preset <presetName>’, ‘Skip prompts and use saved or remote preset’)
.option(‘-d, –default’, ‘Skip prompts and use default preset’)
.option(‘-i, –inlinePreset <json>’, ‘Skip prompts and use inline JSON string as preset’)
.option(‘-m, –packageManager <command>’, ‘Use specified npm client when installing dependencies’)
.option(‘-r, –registry <url>’, ‘Use specified npm registry when installing dependencies (only for npm)’)
.option(‘-g, –git [message]’, ‘Force git initialization with initial commit message’)
.option(‘-n, –no-git’, ‘Skip git initialization’)
.option(‘-f, –force’, ‘Overwrite target directory if it exists’)
.option(‘-c, –clone’, ‘Use git clone when fetching remote preset’)
.option(‘-x, –proxy’, ‘Use specified proxy when creating project’)
.option(‘-b, –bare’, ‘Scaffold project without beginner instructions’)
.option(‘–skipGetStarted’, ‘Skip displaying “Get started” instructions’)
.action((name, cmd) => {
const options = cleanArgs(cmd)

if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow(‘\n Info: You provided more than one argument. The first one will be used as the app\’s name, the rest are ignored.’))
}
// –git makes commander to default git to true
if (process.argv.includes(‘-g’) || process.argv.includes(‘–git’)) {
options.forceGit = true
}
require(‘../lib/create’)(name, options)
})
这边创建了一个 command(‘create <app-name>’), 如果是全局安装了 @vue-cli3.0, 那么就可以使用
vue create xxxx yyy
xxx yyy 为文件名称和 option 这些参数配置项。我们来看一下分别有哪些配置项:
-p, –preset <presetName> 忽略提示符并使用已保存的或远程的预设选项
-d, –default 忽略提示符并使用默认预设选项
-i, –inlinePreset <json> 忽略提示符并使用内联的 JSON 字符串预设选项
-m, –packageManager <command> 在安装依赖时使用指定的 npm 客户端
-r, –registry <url> 在安装依赖时使用指定的 npm registry
-g, –git [message] 强制 / 跳过 git 初始化,并可选的指定初始化提交信息
-n, –no-git 跳过 git 初始化
-f, –force 覆写目标目录可能存在的配置
-c, –clone 使用 git clone 获取远程预设选项
-x, –proxy 使用指定的代理创建项目
-b, –bare 创建项目时省略默认组件中的新手指导信息
-h, –help 输出使用帮助信息

在 action 中就是进入命令后的执行代码, 以下部分主要就是提取 - g 命令,
const options = cleanArgs(cmd)

if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow(‘\n Info: You provided more than one argument. The first one will be used as the app\’s name, the rest are ignored.’))
}
// –git makes commander to default git to true
if (process.argv.includes(‘-g’) || process.argv.includes(‘–git’)) {
options.forceGit = true
}
2. 基础信息验证
接下来就是进入 create.js 文件
async function create (projectName, options) {
// 代理使用 -x 或 –proxy 参数配置
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}

const cwd = options.cwd || process.cwd() // 当前目录
const inCurrent = projectName === ‘.’ // 是否存在当前目录
const name = inCurrent ? path.relative(‘../’, cwd) : projectName // 项目名称
const targetDir = path.resolve(cwd, projectName || ‘.’) // 生成项目目录

const result = validateProjectName(name) // 验证名称是否符合规范
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: “${name}”`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim(‘Error: ‘ + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim(‘Warning: ‘ + warn))
})
exit(1)
}
// 检测文件是否存在,
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir)
// 这边强制覆盖
} else {
await clearConsole()
if (inCurrent) {
// 这边提示是否在当前文件创建?
const {ok} = await inquirer.prompt([
{
name: ‘ok’,
type: ‘confirm’,
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
// 文件已重复
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’},
{name: ‘Cancel’, value: false}
]
}
])
if (!action) {
return
} else if (action === ‘overwrite’) {
console.log(`\nRemoving ${chalk.cyan(targetDir)}…`)
await fs.remove(targetDir)
}
}
}
}
// 新建构造器
const creator = new Creator(name, targetDir, getPromptModules()) // getPromptModules() 为内置插件对话对象
await creator.create(options)
}

以上大部分都是定义文件,目录和一名称效验,文件效验,比较简单易懂,接下来就是创建 Creator 构造器了
3.Creator 构造器
这一节的内容会比较绕一点。首先我们先来了解一下 vue-cli-preset, 这是一个包含创建新项目所需预定义选项和插件的 JSON 对象,让用户无需在命令提示中选择它们。vue create 过程中保存的 preset 会被放在你的用户目录下的一个配置文件中 (~/.vuerc)。你可以通过直接编辑这个文件来调整、添加、删除保存好的 preset。这里有一个 preset 的示例:
{
“useConfigFiles”: true,
“router”: true,
“vuex”: true,
“cssPreprocessor”: “sass”,
“plugins”: {
“@vue/cli-plugin-babel”: {},
“@vue/cli-plugin-eslint”: {
“config”: “airbnb”,
“lintOn”: [“save”, “commit”]
}
}
}
Preset 的数据会被插件生成器用来生成相应的项目文件。除了上述这些字段,你也可以为集成工具添加配置:
{
“useConfigFiles”: true,
“plugins”: {…},
“configs”: {
“vue”: {…},
“postcss”: {…},
“eslintConfig”: {…},
“jest”: {…}
}
}
这些额外的配置将会根据 useConfigFiles 的值被合并到 package.json 或相应的配置文件中。例如,当 “useConfigFiles”: true 的时候,configs 的值将会被合并到 vue.config.js 中。
更多关于 preset 可以前往 vue-cli 官网 插件和 Preset https://cli.vuejs.org/zh/guid…。
在基础验证完后会创建一个 Creator 实例
const creator = new Creator(name, targetDir, getPromptModules())

3.1getPromptModules
在分析 Creator 之前,我们先来看一下 getPromptModules 是什么。getPromptModules 源码
exports.getPromptModules = () => {
return [
‘babel’,
‘typescript’,
‘pwa’,
‘router’,
‘vuex’,
‘cssPreprocessors’,
‘linter’,
‘unit’,
‘e2e’
].map(file => require(`../promptModules/${file}`))
}

我们可以在 promptModules 中分别看到

其中比如 unit.js:
module.exports = cli => {
cli.injectFeature({
name: ‘Unit Testing’,
value: ‘unit’,
short: ‘Unit’,
description: ‘Add a Unit Testing solution like Jest or Mocha’,
link: ‘https://cli.vuejs.org/config/#unit-testing’,
plugins: [‘unit-jest’, ‘unit-mocha’]
})

cli.injectPrompt({
name: ‘unit’,
when: answers => answers.features.includes(‘unit’),
type: ‘list’,
message: ‘Pick a unit testing solution:’,
choices: [
{
name: ‘Mocha + Chai’,
value: ‘mocha’,
short: ‘Mocha’
},
{
name: ‘Jest’,
value: ‘jest’,
short: ‘Jest’
}
]
})

cli.onPromptComplete((answers, options) => {
if (answers.unit === ‘mocha’) {
options.plugins[‘@vue/cli-plugin-unit-mocha’] = {}
} else if (answers.unit === ‘jest’) {
options.plugins[‘@vue/cli-plugin-unit-jest’] = {}
}
})
}
我们可以看到这部其实就是对一些内置插件的一些配置项,用于对话后来进行安装,
cli.injectFeature:是用来注入 featurePrompt,即初始化项目时,选择的 babel、typescript 等
cli.injectPrompt:是根据选择的 featurePrompt 然后注入对应的 prompt,当选择了 unit,接下来会有以下的 prompt,选择 Mocha + Chai 还是 Jest
cli.onPromptComplete:就是一个回调,会根据选择来添加对应的插件,当选择了 mocha,那么就会添加 @vue/cli-plugin-unit-mocha 插件
3.2 new Creator
接下来我们来看一下其构造函数
constructor (name, context, promptModules) {
super()

this.name = name // 目录名称
this.context = process.env.VUE_CLI_CONTEXT = context // 当前目录
const {presetPrompt, featurePrompt} = this.resolveIntroPrompts() // 之前预制的插件,和项目的一些 feature
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts() // 其他的插件
this.injectedPrompts = [] // 当选择 featurePrompt 时,注入的 prompts
this.promptCompleteCbs = []
this.createCompleteCbs = []

this.run = this.run.bind(this)

const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
}

上述代码我们主要来看一下 PromptModuleAPI,其他都是一些变量初始化的定义
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}

injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}

injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}

injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}

onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}

这边是创建一个 PromptModuleAPI 实例,并通过 promptModules.forEach(m => m(promptAPI)),将预设的内置插件加入到 this.creator.featurePrompt,this.creator.injectedPrompts 和 this.creator.promptCompleteCbs 中
3.3getPreset
在创建了 Creator 实例后,然后调用了 create 方法
await creator.create(options)
create 方法源码,这段代码比较简单,主要是判断是否有 -p,-d,- i 的配置项来直接安装,如果没有的话,就进入对话模式 this.promptAndResolvePreset,来选择性的安装
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
const {run, name, context, createCompleteCbs} = this

if (!preset) {
// 是否存在 -p 或 –preset
if (cliOptions.preset) {
// vue create foo –preset bar
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else if (cliOptions.default) {
// 是否有 - d 或 –default 的命令,如果有则,默认直接安装
// vue create foo –default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) {
// 是否有 –inlinePreset 或 - i 来注入插件
// vue create foo –inlinePreset {…}
try {
preset = JSON.parse(cliOptions.inlinePreset)
} catch (e) {
error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
exit(1)
}
} else {
preset = await this.promptAndResolvePreset()
}
}
先判断 vue create 命令是否带有 -p 选项,如果有的话会调用 resolvePreset 去解析 preset。resolvePreset 函数会先获取 ~/.vuerc 中保存的 preset,然后进行遍历,如果里面包含了 -p 中的 <presetName>,则返回~/.vuerc 中的 preset。如果没有则判断是否是采用内联的 JSON 字符串预设选项,如果是就会解析 .json 文件,并返回 preset,还有一种情况就是从远程获取 preset(利用 download-git-repo 下载远程的 preset.json)并返回。
上面的情况是当 vue create 命令带有 -p 选项的时候才会执行,如果没有就会调用 promptAndResolvePreset 函数利用 inquirer.prompt 以命令后交互的形式来获取 preset,下面看下 promptAndResolvePreset 函数的源码:
async promptAndResolvePreset (answers = null) {
// prompt
if (!answers) {
await clearConsole(true)
answers = await inquirer.prompt(this.resolveFinalPrompts())
// 交互式命令对话,安装 defalut 和 Manually select features
}
debug(‘vue-cli:answers’)(answers)

if (answers.packageManager) {
saveOptions({
packageManager: answers.packageManager
})
}

let preset
if (answers.preset && answers.preset !== ‘__manual__’) {// 如果是选择本地保存的 preset(.vuerc)
preset = await this.resolvePreset(answers.preset)
} else {
// manual
preset = {
useConfigFiles: answers.useConfigFiles === ‘files’,
plugins: {}
}
answers.features = answers.features || []
// run cb registered by prompt modules to finalize the preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
}

// validate
validatePreset(preset)

// save preset
if (answers.save && answers.saveName) {
savePreset(answers.saveName, preset)
}

debug(‘vue-cli:preset’)(preset)
return preset
}
看到这里会比较乱的,preset 会比较多,我们再来看一下 resolveFinalPrompts 源码
resolveFinalPrompts () {
// patch generator-injected prompts to only show in manual mode
this.injectedPrompts.forEach(prompt => {
const originalWhen = prompt.when || (() => true)
prompt.when = answers => {
return isManualMode(answers) && originalWhen(answers)
}
})
const prompts = [
this.presetPrompt,
this.featurePrompt,
…this.injectedPrompts,
…this.outroPrompts
]
debug(‘vue-cli:prompts’)(prompts)
console.log(1, prompts)
return prompts
}
这里我们可以看到 presetPrompt,featurePrompt,injectedPrompts,outroPrompts 合并成一个数组进行返回,presetPrompt 是预设的,当上一次选择 manually 模式进行了预设,并保存到.vuerc 中,那么初始化的时候会列出已保存的插件 featurePrompt 就是内置的一些插件 injectedPrompts 是通过 - i 命令来手动注入的插件 outroPrompts 是一些其他的插件。这边对话完之后,就要开始依赖的安装了。
4 依赖安装
我们继把 create 中的代码往下走
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? ‘yarn’ : ‘npm’)
)

await clearConsole() // 清空控制台
logWithSpinner(`✨`, `Creating project in ${chalk.yellow(context)}.`)
this.emit(‘creation’, { event: ‘creating’})

// get latest CLI version
const {latest} = await getVersions()
const latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0`
// generate package.json with plugin dependencies
const pkg = {
name,
version: ‘0.1.0’,
private: true,
devDependencies: {}
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
if (preset.plugins[dep]._isPreset) {
return
}

// Note: the default creator includes no more than `@vue/cli-*` & `@vue/babel-preset-env`,
// so it is fine to only test `@vue` prefix.
// Other `@vue/*` packages’ version may not be in sync with the cli itself.
pkg.devDependencies[dep] = (
preset.plugins[dep].version ||
((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
)
})
// write package.json
await writeFileTree(context, {
‘package.json’: JSON.stringify(pkg, null, 2)
})
这边主要就是获取 cli 的版本和生产 package.json,其中主要是获取版本号
module.exports = async function getVersions () {
if (sessionCached) {
return sessionCached
}

let latest
const local = require(`../../package.json`).version
if (process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG) {
return (sessionCached = {
current: local,
latest: local
})
}

const {latestVersion = local, lastChecked = 0} = loadOptions()
const cached = latestVersion
const daysPassed = (Date.now() – lastChecked) / (60 * 60 * 1000 * 24)

if (daysPassed > 1) {// 距离上次检查更新超过一天
// if we haven’t check for a new version in a day, wait for the check
// before proceeding
latest = await getAndCacheLatestVersion(cached)
} else {
// Otherwise, do a check in the background. If the result was updated,
// it will be used for the next 24 hours.
getAndCacheLatestVersion(cached) // 后台更新
latest = cached
}

return (sessionCached = {
current: local,
latest
})
}

// fetch the latest version and save it on disk
// so that it is available immediately next time
async function getAndCacheLatestVersion (cached) {
const getPackageVersion = require(‘./getPackageVersion’)
const res = await getPackageVersion(‘vue-cli-version-marker’, ‘latest’)
if (res.statusCode === 200) {
const {version} = res.body
if (semver.valid(version) && version !== cached) {
saveOptions({latestVersion: version, lastChecked: Date.now() })
return version
}
}
return cached
}
这边主要是有 2 个版本变量,一个是 local 本地 cli 版本。另一个 laset 远程 cli 版本另外 getAndCacheLatestVersion 而是通过 vue-cli-version-marker npm 包获取的 CLI 版本。生产 package.json 之后,我们在继续看后面代码
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
logWithSpinner(`????`, `Initializing git repository…`)
this.emit(‘creation’, { event: ‘git-init’})
await run(‘git init’)
}

// install plugins
stopSpinner()
log(`⚙ Installing CLI plugins. This might take a while…`)
log()
this.emit(‘creation’, { event: ‘plugins-install’})
if (isTestOrDebug) {
// in development, avoid installation process
await require(‘./util/setupDevProject’)(context)
} else {
await installDeps(context, packageManager, cliOptions.registry)
}
这段代码首先会调用 shouldInitGit 来判断是否需要 git 初始化,判断的情景是:是否安装了 git;命令中是否有 - g 或 –git, 或 –no-git 或 -n;生成的目录是否包含了 git 判断完之后需要 git 初始化项目后,接下来就会调用 installDeps 来安装依赖
exports.installDeps = async function installDeps (targetDir, command, cliRegistry) {
const args = []
if (command === ‘npm’) {
args.push(‘install’, ‘–loglevel’, ‘error’)
} else if (command === ‘yarn’) {
// do nothing
} else {
throw new Error(`Unknown package manager: ${command}`)
}

await addRegistryToArgs(command, args, cliRegistry)

debug(`command: `, command) // DEBUG=vue-cli:install vue create demo
debug(`args: `, args)

await executeCommand(command, args, targetDir)
}

退出移动版