Vue源码解析系列(十八) -- Vue-cli的源码解读
前言
本篇旨在讲解vue-cli
源码,通过
npm install -g @vue/cli
# OR
yarn global add @vue/cli
就可以在全局安装vue-cli
,通过vue create [projectName]
来创建一个快速开发的模板。有人说你怎么知道此中含有create
的方法呢?在终端中打出vue
,会去执行bin/vue.js
,他就会有如此的提示。
如果当前机器node
版本低于指定版本,就会报错:
那么我们打开vue-cli的源码,在
vue-cli/packages/@vue/cli/bin/vue.js
目录下,我们可以看到vue-cli
的入口文件。
// vue-cli/packages/@vue/cli/bin/vue.js
const { chalk, semver } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node // "^12.0.0 || >= 14.0.0"
const leven = require('leven')
// 检查本地nodejs版本
function checkNodeVersion (wanted, id) {
if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
console.log(chalk.red(
'You are using Node ' + process.version + ', but this version of ' + id +
' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
))
process.exit(1)
}
}
checkNodeVersion(requiredVersion, '@vue/cli')
const fs = require('fs')
const path = require('path')
const slash = require('slash')
const minimist = require('minimist')
// enter debug mode when creating test repo
if (
slash(process.cwd()).indexOf('/packages/test') > 0 && (
fs.existsSync(path.resolve(process.cwd(), '../@vue')) ||
fs.existsSync(path.resolve(process.cwd(), '../../@vue'))
)
) {
process.env.VUE_CLI_DEBUG = true
}
...
在此文件中,当我们键入vue
命令,就会开始执行checkNodeVersion
函数,之后便是控制台的command
选项。这里涉及到了一个commander
包,我们简单认识一下他。
commander
- 描述:这是一个使用子命令并带有帮助描述的更完整的程序。在多命令程序中,每个命令(或命令的独立可执行文件)都有一个操作处理程序。
- 使用:
program.command('split')
.description('Split a string into substrings and display as an array')
.argument('<string>', 'string to split')
.option('--first', 'display just the first substring')
.option('-s, --separator <char>', 'separator character', ',')
.action((str, options) => {
const limit = options.first ? 1 : undefined;
console.log(str.split(options.separator, limit));
});
我们便知道可以借用commander
包来实现在终端输出内容了,比如我们来看一下vue create [projectName]
:
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('--merge', 'Merge target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
.option('-b, --bare', 'Scaffold project without beginner instructions')
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, options) => {
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
}
// 加载create函数, 传入name与option
require('../lib/create')(name, options)
})
-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 输出使用帮助信息
流程
看此图我们可以了解到从敲下命令vue create [projectName]
到模板的呈现的完整步骤。
- 验证
- 预设配置项
- 依赖安装
- 模板生成
- 基础项目输出
验证
async function create (projectName, options) {
// 使用指定的代理创建项目
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) && !options.merge) {
// 强制移除目标目录
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)
}
}
}
}
// 创建creator
// getPromptModules 获取预设模块
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
在验证阶段做了项目名验证
,这个验证主要是看看我们写的项目名是都符合规范而已,不去深究,再者做了覆盖当前目录的方式,vue
作为全民公用框架,比不了私服脚手架
,在我们定制cli
的时候,不需要去考虑这些。
预设获取
之后就是我们需要选择的preset
预设,他能够让你可选性的安装配置,我们这里选择vue2
我们先来看一下工程目录:
package.json
{
"name": "my", // 项目名称
"version": "0.1.0", // 项目版本
"private": true, // 私有化
"scripts": { // 启动指令
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": { // 依赖
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": { // 依赖
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": { // eslint配置
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [ // 浏览器相关
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
这样我们就初始化了一个vue2的项目,我们可以看到在过程中vue-cli(V5.0.8)
已经放弃了以前的多选配置项(比如eslint
、babel
),都作为默认的配置加载到了工程化文件当中。那么这一步是怎么做的呢,我们接下来看一下new Creator
这个过程。
首先我们来分析一下Creator
这个类。
constructor (name, context, promptModules) {
super()
this.name = name // 项目名
this.context = process.env.VUE_CLI_CONTEXT = context // 上下文
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() // 获取了 presetPrompt list,在初始化项目的时候提供选择
this.presetPrompt = presetPrompt // presetPrompt list
this.featurePrompt = featurePrompt // babal, pwa, e2e etc.
this.outroPrompts = this.resolveOutroPrompts() // 存放项目配置的文件(package.json || congfig.js) 以及是否将 presetPrompts 存放起来
this.injectedPrompts = [] // 对应 feature 的 Prompts
this.promptCompleteCbs = [] // injectedPrompts 的回调
this.createCompleteCbs = []
this.run = this.run.bind(this)
const promptAPI = new PromptModuleAPI(this)
/**
* 1. 将 babel, e2e, pwa 等 push 到 featurePrompt.choices 中,在选择项目需要配置哪些时显示出来 (checkbox);
* 2. 将 babel, e2e, pwa 等 push 到 injectedPrompts 中,当设置了 feature 会对应通过 Prompts 来进一步选择哪种模式,比如当选择了 E2E Testing ,然后会再次让你
* 选择哪种 E2E Testing,即, Cypress (Chrome only) || Nightwatch (Selenium-based);
* 3. 将每中 feature 的 onPromptComplete push 到 promptCompleteCbs,在后面会根据选择的配置来安装对应的 plugin。
*/
promptModules.forEach(m => m(promptAPI))
}
PromptModuleAPI
PromptModuleAPI
实例会调用它的实例方法,然后将 injectFeature
, injectPrompt
, injectOptionForPrompt
, onPromptComplete
保存到 Creator
实例对应的变量中。
最后遍历 getPromptModules
获取的 promptModules
,传入实例 promptAPI
,初始化 Creator
实例中 featurePrompt
, injectedPrompts
, promptCompleteCbs
变量。
getPromptModules
getPromptModules
出现在new Creator
的参数中,getPromptModules
就是按照目录去加载插件包的。
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e',
'webpackPreset'
].map(file => require(`../promptModules/${file}`))
}
比如我们加上一个插件webpackPreset
,则会在终端输出:
那我们知道getPromptModules
是让用户选择配置项的,那么每一个配置里面会是怎么样的呢?我们来看一下Router.js
文件。
// Router.js
const { chalk } = require('@vue/cli-shared-utils')
// 在终端弹出配置可选Router属性
module.exports = cli => {
cli.injectFeature({
name: 'Router', // 名字
value: 'router', // 值
description: 'Structure the app with dynamic pages', // 描述
link: 'https://router.vuejs.org/' // 文档链接
})
//在终端弹出配置可选项
cli.injectPrompt({
name: 'historyMode', // 默认history模式
when: answers => answers.features.includes('router'), // 当配置可选中出现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'
})
// 选择回调,当有选择router的时候,加载官方插件@vue/cli-plugin-router
cli.onPromptComplete((answers, options) => {
if (answers.features.includes('router')) {
options.plugins['@vue/cli-plugin-router'] = {
historyMode: answers.historyMode
}
}
})
}
至此getPromptModules
中做的事情就完了,我们继续看一下new Creator
做的事情。
creator.create
实例调用create
方法,需要的是一个异步过程。
async create(cliOptions = {}, preset = null) {
// create函数 接受cliOptions和preset两个参数,因为在调用的时候create(options)只给了1个参数
// 判断是tes环境还是debug环境
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG;
// 获取Creator类的属性与方法
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this;
// 一开始进来 preset为null,
if (!preset) {
if (cliOptions.preset) { // 判断 -p选项
// vue create foo --preset bar
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone);
} else if (cliOptions.default) { // 判断 -d 选项
// vue create foo --default
preset = defaults.presets["Default (Vue 3)"];
} else if (cliOptions.inlinePreset) { // 如果是行内参数,就要去解析他
// 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 {
// vue create project, 通过promptAndResolvePreset去加载preset
preset = await this.promptAndResolvePreset();
}
}
// clone before mutating
preset = cloneDeep(preset);
// inject core service 注入核心@vue/cli-service
preset.plugins["@vue/cli-service"] = Object.assign(
{
projectName: name,
},
preset
);
if (cliOptions.bare) { // 创建项目时省略默认组件中的新手指导信息
preset.plugins["@vue/cli-service"].bare = true;
}
// legacy support for router
if (preset.router) { // 注入router插件
preset.plugins["@vue/cli-plugin-router"] = {};
if (preset.routerHistoryMode) { // 更改router模式
preset.plugins["@vue/cli-plugin-router"].historyMode = true;
}
}
// legacy support for vuex
if (preset.vuex) { // 注入vuex插件
preset.plugins["@vue/cli-plugin-vuex"] = {};
}
先判断 vue create
命令是否带有 -p 选项,如果有的话会调用 resolvePreset
去解析 preset。
async resolvePreset(name, clone) {
let preset;
const savedPresets = this.getPresets();
// getPresets
getPresets() {
const savedOptions = loadOptions(); // 获取~/.vuerc中保存的预设
return Object.assign({}, savedOptions.presets, defaults.presets);
}
if (name in savedPresets) {
preset = savedPresets[name];
} else if (name === "default") {
preset = savedPresets["Default (Vue 3)"];
} else if ( // 获取.json文件预设
name.endsWith(".json") ||
/^\./.test(name) ||
path.isAbsolute(name)
) {
preset = await loadLocalPreset(path.resolve(name));
} else if (name.includes("/")) {
log(`Fetching remote preset ${chalk.cyan(name)}...`);
this.emit("creation", { event: "fetch-remote-preset" });
try {
preset = await loadRemotePreset(name, clone); // 通过远程下载preset
} catch (e) {
error(`Failed fetching remote preset ${chalk.cyan(name)}:`);
throw e;
}
}
if (!preset) {
error(`preset "${name}" not found.`);
const presets = Object.keys(savedPresets);
if (presets.length) {
log();
log(`available presets:\n${presets.join(`\n`)}`);
} else {
log(`you don't seem to have any saved preset.`);
log(`run vue-cli in manual mode to create a preset.`);
}
exit(1);
}
return preset;
}
resolvePreset
函数会先获取~/.vuerc
中保存的preset
, 然后进行遍历,如果里面包含了-p
中的<presetName>
,则返回~/.vuerc
中的preset
。- 如果没有则判断是否是采用内联的
JSON
字符串预设选项,如果是就会解析.json
文件,并返回preset.
- 还有一种情况就是从远程获取
prese
(利用download-git-repo
下载远程的preset.json
)并返回。
promptAndResolvePreset
如果不带任何参数,则会调用promptAndResolvePreset
去让我们手动去获取preset
。
async promptAndResolvePreset(answers = null) {
// prompt
if (!answers) {
await clearConsole(true); // 清空控制台
answers = await inquirer.prompt(this.resolveFinalPrompts()); // inquirer实现选择交互
/**
* resolveFinalPrompts() {
// 把getPromptsModule中的配置项与特性遍历执行
this.injectedPrompts.forEach((prompt) => {
const originalWhen = prompt.when || (() => true);
prompt.when = (answers) => { // 当时answer被选择了ManuallySelect的时候
return isManualMode(answers) && originalWhen(answers);
};
});
// 加入特性选项弹窗
const prompts = [
this.presetPrompt,// 如果上一次的自定义预设保存到了~/.vuerc中,那么这一次会显示出来
this.featurePrompt, // 项目的一些 feature,就是选择 babel,
// typescript,pwa,router,vuex,
// cssPreprocessors,linter,unit,e2e。
...this.injectedPrompts, // 当你选择了feature之后就会注入模式,
// 比如你选择了 unit,那么就会让你选择模式:
// `Mocha + Chai` 还是 `Jest`
...this.outroPrompts, // 是否写在package.json里面还是单独成config,
// 使用yarn还是npm等
];
debug("vue-cli:prompts")(prompts);
return prompts;
}
*/
}
debug("vue-cli:answers")(answers);
// 自定义中会让你选择 npm 或者pnpm 或者yarn来管理包
if (answers.packageManager) {
saveOptions({
packageManager: answers.packageManager,
});
}
let preset;
// 如果没有选择ManuallySelect,则会根据resolvePreset来加载preset
if (answers.preset && answers.preset !== "__manual__") {
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)
) {
log();
log(
`🎉 Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(
rcPath
)}`
);
}
debug("vue-cli:preset")(preset);
return preset;
}
依赖安装
包管理工具
// 使用npm 、yarn 、pnpm来管理包
const packageManager =
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? "yarn" : null) ||
(hasPnpm3OrLater() ? "pnpm" : "npm");
await clearConsole(); // 清空控制台
// 获得包管理器实例
const pm = new PackageManager({
context,
forcePackageManager: packageManager,
});
// 打印 在当前目录创建项目成功
log(`✨ Creating project in ${chalk.yellow(context)}.`);
this.emit("creation", { event: "creating" });
生成package.json文件
// 获得cli插件最新版本
const { latestMinor } = await getVersions();
// 生成package.json文件
const pkg = {
name,
version: "0.1.0",
private: true,
devDependencies: {},
...resolvePkg(context),
};
const deps = Object.keys(preset.plugins);
deps.forEach((dep) => {
if (preset.plugins[dep]._isPreset) {
return;
}
let { version } = preset.plugins[dep];
if (!version) {
if (
isOfficialPlugin(dep) ||
dep === "@vue/cli-service" ||
dep === "@vue/babel-preset-env"
) {
version = isTestOrDebug ? `latest` : `~${latestMinor}`;
} else {
version = "latest";
}
}
pkg.devDependencies[dep] = version;
});
写入package.json文件
await writeFileTree(context, {
"package.json": JSON.stringify(pkg, null, 2),
});
// generate a .npmrc file for pnpm, to persist the
// `shamefully-flatten` flag
if (packageManager === "pnpm") {
const pnpmConfig = hasPnpmVersionOrLater("4.0.0")
? // pnpm v7 makes breaking change to
// set strict-peer-dependencies=true by default,
// which may cause some problems
// when installing
"shamefully-hoist=true\nstrict-peer-dependencies=false\n"
: "shamefully-flatten=true\n";
await writeFileTree(context, {
".npmrc": pnpmConfig,
});
}
初始化git仓库
// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
const shouldInitGit = this.shouldInitGit(cliOptions);
if (shouldInitGit) {
log(`🗃 Initializing git repository...`);
this.emit("creation", { event: "git-init" });
await run("git init");
}
安装插件
// install plugins
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`);
log();
this.emit("creation", { event: "plugins-install" });
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
// in development, avoid installation process
await require("./util/setupDevProject")(context);
} else {
await pm.install();
}
Invoking generators
log(`🚀 Invoking generators...`);
this.emit("creation", { event: "invoking-generators" });
const plugins = await this.resolvePlugins(preset.plugins, pkg);
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs,
});
await generator.generate({
extractConfigFiles: preset.useConfigFiles,
});
install additional deps
// install additional deps (injected by generators)
log(`📦 Installing additional dependencies...`);
this.emit("creation", { event: "deps-install" });
log();
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
await pm.install();
}
Running completion hooks
// run complete cbs if any (injected by generators)
log(`⚓ Running completion hooks...`);
this.emit("creation", { event: "completion-hooks" });
for (const cb of afterInvokeCbs) {
await cb();
}
for (const cb of afterAnyInvokeCbs) {
await cb();
}
生成README.md
if (!generator.files["README.md"]) {
// generate README.md
log();
log("📄 Generating README.md...");
await writeFileTree(context, {
"README.md": generateReadme(generator.pkg, packageManager),
});
}
上传README.md到仓库
let gitCommitFailed = false;
if (shouldInitGit) {
await run("git add -A");
if (isTestOrDebug) {
await run("git", ["config", "user.name", "test"]);
await run("git", ["config", "user.email", "test@test.com"]);
await run("git", ["config", "commit.gpgSign", "false"]);
}
const msg = typeof cliOptions.git === "string" ? cliOptions.git : "init";
try {
await run("git", ["commit", "-m", msg, "--no-verify"]);
} catch (e) {
gitCommitFailed = true;
}
}
generator.printExitLogs();
cd [projectName]
npm || pnpm || yarn serve
log();
log(`🎉 Successfully created project ${chalk.yellow(name)}.`);
if (!cliOptions.skipGetStarted) {
log(
`👉 Get started with the following commands:\n\n` +
(this.context === process.cwd()
? ``
: chalk.cyan(` ${chalk.gray("$")} cd ${name}\n`)) +
chalk.cyan(
` ${chalk.gray("$")} ${
packageManager === "yarn"
? "yarn serve"
: packageManager === "pnpm"
? "pnpm run serve"
: "npm run serve"
}`
)
);
}
log();
this.emit("creation", { event: "done" });
if (gitCommitFailed) {
warn(
`Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
`You will need to perform the initial commit yourself.\n`
);
}
generator.printExitLogs();
}
npm run serve
在项目成功初始化之后终端会出现
cd projectName 进入目录
npm serve 启动服务
在package.json
里面我们也可以看到,vue-cli-service
启动serve
的:
vue-cli-service
#!/usr/bin/env node
const { semver, error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node
...
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
boolean: [
// build
// FIXME: --no-module, --no-unsafe-inline, no-clean, etc.
'modern',
'report',
'report-json',
'inline-vue',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
const command = args._[0]
// 调用service,run方法,启动服务,用catch来捕捉异常。
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
其中run方法接受三个参数。
command
函数,表示为一个指令,比如serve
、build
等。
async run(name, args = {}, rawArgv = []) {
// serve {} ['serve']
...
const { fn } = command;
return fn(args, rawArgv); // return serve(args, rawArgs)
}
这里的fn
函数就是command
函数命令,传入的是serve
。
那么重头戏来了,vue-cli-serve
通过动态注册api.registerCommand
指令,注册serve
方法,所以fn
的执行就是serve
的执行。
工具方法与config
const {
info,
error,
hasProjectYarn,
hasProjectPnpm,
IpcMessenger,
} = require("@vue/cli-shared-utils"); // 工具方法
const getBaseUrl = require("../util/getBaseUrl"); // 获取基础路径
const defaults = {
host: "0.0.0.0",
port: 8080,
https: false,
}; // 默认配置 端口,主机名 协议
serve.js
module.exports = (api, options) => {
const baseUrl = getBaseUrl(options);
// registerCommand(
// name: string,
// opts: RegisterCommandOpts,
// fn: RegisterCommandFn
// ): void
api.registerCommand(
"serve",
{
description: "start development server",
usage: "vue-cli-service serve [options] [entry]",
options: {
"--open": `open browser on server start`,
"--copy": `copy url to clipboard on server start`,
"--stdin": `close when stdin ends`,
"--mode": `specify env mode (default: development)`,
"--host": `specify host (default: ${defaults.host})`,
"--port": `specify port (default: ${defaults.port})`,
"--https": `use https (default: ${defaults.https})`,
"--public": `specify the public network URL for the HMR client`,
"--skip-plugins": `comma-separated list of
plugin names to skip for this run`,
},
},
async function serve(args) {
...code
}
);
};
那我们紧接着来看一下serve
方法干了什么事情。
serve函数
// serve.js
async function serve(args) {
info("Starting development server..."); // 给个提示框:服务启动
const isInContainer = checkInContainer(); // 容器检查
const isProduction = process.env.NODE_ENV === "production"; // 生产环境判断
const { chalk } = require("@vue/cli-shared-utils");
// webpack webpack-dev-server相关
// 可见vue-cli的本地服务,也是借助于webpack-dev-server实现的
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const portfinder = require("portfinder");
const prepareURLs = require("../util/prepareURLs");
const prepareProxy = require("../util/prepareProxy");
const launchEditorMiddleware = require("launch-editor-middleware");
const validateWebpackConfig = require("../util/validateWebpackConfig");
const isAbsoluteUrl = require("../util/isAbsoluteUrl");
用chainWebpack注入webpack配置
// configs that only matters for dev server
api.chainWebpack((webpackConfig) => {
if (
process.env.NODE_ENV !== "production" &&
process.env.NODE_ENV !== "test"
) {
if (!webpackConfig.get("devtool")) {
webpackConfig.devtool("eval-cheap-module-source-map");
}
// https://github.com/webpack/webpack/issues/6642
// https://github.com/vuejs/vue-cli/issues/3539
webpackConfig.output.globalObject(
`(typeof self !== 'undefined' ? self : this)`
);
if (
!process.env.VUE_CLI_TEST &&
(!options.devServer.client ||
options.devServer.client.progress !== false)
) {
// the default progress plugin
// won't show progress due to infrastructreLogging.level
webpackConfig
.plugin("progress")
.use(require("progress-webpack-plugin"));
}
}
});
resolveWebpackConfig获取webpack配置
resolveWebpackConfig
函数传入了一个 chainableConfig
参数,这个参数是以 webpack-chain
形式注入的 webpack
配置,看下如何获取 webpack-chain
形式的 webpack
配置:
// 解析webpack配置
const webpackConfig = api.resolveWebpackConfig();
resolveWebpackConfig
resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
if (!this.initialized) {
throw new Error('Service must call init() before calling resolveWebpackConfig().')
}
// get raw config
// './config/base', './config/css', './config/dev', './config/prod', './config/app' 这 5 个内置插件的主要作用
// 就是完成 webpack 本地编译构建时的各种相关的配置,注意下这几个插件的顺序,因为他们都是利用 api.chainWebpack 进行 webpack 配置的,
// 而配置又是可以覆盖的,因此如果拥有相同的配置,后面加载的插件的配置会覆盖前面的,但优先级最高的还是项目配置中的 webpack 配置,因为
// vue.config.js 或者 package.vue 中的 webpack 配置是最后解析的。
let config = chainableConfig.toConfig() // 导出 webpack 配置对象
const original = config
// apply raw config fns
// raw 式配置,传入 webpackChain 的配置
this.webpackRawConfigFns.forEach(fn => {
if (typeof fn === 'function') {
// function with optional return value
const res = fn(config)
if (res) config = merge(config, res)
} else if (fn) {
// merge literal values
config = merge(config, fn)
}
})
...
return config
}
resolveChainableWebpackConfig
resolveChainableWebpackConfig () {
const chainableConfig = new Config()
// apply chains
this.webpackChainFns.forEach(fn => fn(chainableConfig))
return chainableConfig
}
-
这就是获取
webpack-chain
的配置,代码很简洁,遍历执行Service
实例中的webpackChainFns
方法,合并所有插件以及项目配置中的webpack-chain
配置,并最终生成项目的webpack-chain
配置。 -
获取了
webpack-chain
形式的配置后,接下来就获取raw
式的webpack
配置(普通的 webpack 配置形式,或者说原生的webpack
配置)。获取方法就是 遍历执行Service
实例中的configureWebpack
方法,并与webpack-chain
形式的配置合并,生成项目最终的 webpack 配置。
获取 devServer 配置
获取 devServer 配置指的是获取 webpack-dev-server 配置,主要有两个地方可以配置, 第一种就是直接在 webpack 中配置,另外一种就是在 vue.config.js
或者 package.vue
中配置,后者配置方式拥有更高地优先级。在获取用户配置的 devServer
以后,还会对这些配置进行解析,比如用户没有配置,会使用默认的 devServer
配置,另外 CLI 参数或者 process.env
中 devServer
拥有更高的优先级,以 devServer.port
为例, vue.config.js
如下:
// vue.config.js
module.exports = {
devServer: {
port: 8081
},
configureWebpack: {
devServer: {
port: 8082
}
}
}
用户自定义devServer与default devServer
const projectDevServerOptions = Object.assign(
webpackConfig.devServer || {},
options.devServer
);
处理一些配置
// resolve server options
const modesUseHttps = ["https", "http2"];
const serversUseHttps = ["https", "spdy"];
const optionsUseHttps =
modesUseHttps.some((modeName) => !!projectDevServerOptions[modeName]) ||
(typeof projectDevServerOptions.server === "string" &&
serversUseHttps.includes(projectDevServerOptions.server)) ||
(typeof projectDevServerOptions.server === "object" &&
projectDevServerOptions.server !== null &&
serversUseHttps.includes(projectDevServerOptions.server.type));
const useHttps = args.https || optionsUseHttps || defaults.https;
const protocol = useHttps ? "https" : "http";
const host =
args.host ||
process.env.HOST ||
projectDevServerOptions.host ||
defaults.host;
portfinder.basePort =
args.port ||
process.env.PORT ||
projectDevServerOptions.port ||
defaults.port;
const port = await portfinder.getPortPromise();
const rawPublicUrl = args.public || projectDevServerOptions.public;
const publicUrl = rawPublicUrl
? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
? rawPublicUrl
: `${protocol}://${rawPublicUrl}`
: null;
const publicHost = publicUrl
? /^[a-zA-Z]+:\/\/([^/?#]+)/.exec(publicUrl)[1]
: undefined;
const urls = prepareURLs(
protocol,
host,
port,
isAbsoluteUrl(baseUrl) ? "/" : baseUrl
);
const localUrlForBrowser = publicUrl || urls.localUrlForBrowser;
const proxySettings = prepareProxy(
projectDevServerOptions.proxy,
api.resolve("public")
);
注入 webpack-dev-server和 hot-reload 中间件入口
webpack-dev-server
和 hot-reload(HRM)
中间件入口。在开发中我们利用 webpack-dev-server
提供一个小型 Express
服务器 ,从而可以为 webpack
打包生成的资源文件提供 web 服务,并用 webpack
自带的 HRM
模块实现热更新。
let webSocketURL;
if (!isProduction) {
if (publicHost) {
// explicitly configured via devServer.public
webSocketURL = {
protocol: protocol === "https" ? "wss" : "ws",
hostname: publicHost,
port,
};
} else if (isInContainer) {
// can't infer public network url if inside a container
// infer it from the browser instead
webSocketURL = "auto://0.0.0.0:0/ws";
} else {
// otherwise infer the url from the config
webSocketURL = {
protocol: protocol === "https" ? "wss" : "ws",
hostname: urls.lanUrlForConfig || "localhost",
port,
};
}
if (process.env.APPVEYOR) {
webpackConfig.plugins.push(
new webpack.EntryPlugin(__dirname, "webpack/hot/poll?500", {
name: undefined,
})
);
}
}
创建 webpack-dev-server 实例
const compiler = webpack(webpackConfig);
// 处理失败的结果
compiler.hooks.failed.tap("vue-cli-service serve", (msg) => {
error(msg);
process.exit(1);
});
// 创建 webpack-dev-server 实例
const server = new WebpackDevServer(
Object.assign(
{
historyApiFallback: {
disableDotRule: true,
htmlAcceptHeaders: ["text/html", "application/xhtml+xml"],
rewrites: genHistoryApiFallbackRewrites(baseUrl, options.pages),
},
hot: !isProduction, // 开发环境下的热更新
},
projectDevServerOptions, // devServer的配置
{
host, // 主机
port, // 端口
server: {
type: protocol,
...(typeof projectDevServerOptions.server === "object"
? projectDevServerOptions.server
: {}),
},
proxy: proxySettings, // 代理
static: { // 静态资源处理
directory: api.resolve("public"),
publicPath: options.publicPath,
watch: !isProduction,
...projectDevServerOptions.static,
},
client: { // 客户端
webSocketURL,
...
},
...
open: args.open || projectDevServerOptions.open, // 在浏览器中打开
setupExitSignals: true,
...
},
}
),
compiler
);
...
// 服务启动
server.start().catch((err) => reject(err));
});
}
完成上述这些操作后,整个项目就运行起来了。
npm run build
npm run build
跟上面一样,都是走的vue-cli-serve
文件入口,执行fn
函数,只不过这里的fn
函数是执行build
函数。
module.exports = (api, options) => {
//动态注册build指令
api.registerCommand('build', {
description: 'build for production',
usage: 'vue-cli-service build [options] [entry|pattern]',
options: {
'--mode': `specify env mode (default: production)`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--no-module': `build app without generating <script type="module"> chunks for modern browsers`,
'--target': `app | lib | wc | wc-async (default: ${defaults.target})`,
'--inline-vue': 'include the Vue module in the final bundle of library or web component target',
'--formats': `list of output formats for library builds (default: ${defaults.formats})`,
'--name': `name for lib or web-component mode (default: "name" in package.json or entry filename)`,
'--filename': `file name for output, only usable for 'lib' target (default: value of --name)`,
'--no-clean': `do not remove the dist directory contents before building the project`,
'--report': `generate report.html to help analyze bundle content`,
'--report-json': 'generate report.json to help analyze bundle content',
'--skip-plugins': `comma-separated list of plugin names to skip for this run`,
'--watch': `watch for changes`,
'--stdin': `close when stdin ends`
}
},
// 绑定build函数
async (args, rawArgs) => {
for (const key in defaults) {
if (args[key] == null) {
args[key] = defaults[key]
}
}
// 获取入口
args.entry = args.entry || args._[0]
if (args.target !== 'app') {
args.entry = args.entry || 'src/App.vue'
}
process.env.VUE_CLI_BUILD_TARGET = args.target
...
args.needsDifferentialLoading = needsDifferentialLoading
if (!needsDifferentialLoading) {
// 文件更改,重新build
await build(args, api, options)
return
}
process.env.VUE_CLI_MODERN_MODE = true
if (!process.env.VUE_CLI_MODERN_BUILD) {
...
await build(legacyBuildArgs, api, options)
...
} else {
...
await build(moduleBuildArgs, api, options)
}
})
}
build
async function build (args, api, options) {
// 引入包
const fs = require('fs-extra')
const path = require('path')
const webpack = require('webpack')
const { chalk } = require('@vue/cli-shared-utils')
const formatStats = require('./formatStats')
const validateWebpackConfig = require('../../util/validateWebpackConfig')
const {
log,
done,
info,
logWithSpinner,
stopSpinner
} = require('@vue/cli-shared-utils')
log()
// mode配置项
const mode = api.service.mode
// 目标终端
if (args.target === 'app') {
const bundleTag = args.needsDifferentialLoading
? args.moduleBuild
? `module bundle `
: `legacy bundle `
: ``
logWithSpinner(`Building ${bundleTag}for ${mode}...`)
} else {
const buildMode = buildModes[args.target]
if (buildMode) {
const additionalParams = buildMode === 'library' ? ` (${args.formats})` : ``
logWithSpinner(`Building for ${mode} as ${buildMode}${additionalParams}...`)
} else {
throw new Error(`Unknown build target: ${args.target}`)
}
}
if (args.dest) {
// Override outputDir before resolving webpack config as config relies on it (#2327)
options.outputDir = args.dest
}
const targetDir = api.resolve(options.outputDir)
const isLegacyBuild = args.needsDifferentialLoading && !args.moduleBuild
获取webpack配置
// resolve raw webpack config
let webpackConfig
if (args.target === 'lib') {
webpackConfig = require('./resolveLibConfig')(api, args, options)
} else if (
args.target === 'wc' ||
args.target === 'wc-async'
) {
webpackConfig = require('./resolveWcConfig')(api, args, options)
} else {
webpackConfig = require('./resolveAppConfig')(api, args, options)
}
// check for common config errors
validateWebpackConfig(webpackConfig, api, options, args.target)
// watch监听
if (args.watch) {
modifyConfig(webpackConfig, config => {
config.watch = true
})
}
if (args.stdin) {
process.stdin.on('end', () => {
process.exit(0)
})
process.stdin.resume()
}
// 打包结果分析
if (args.report || args['report-json']) {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
modifyConfig(webpackConfig, config => {
const bundleName = args.target !== 'app'
? config.output.filename.replace(/\.js$/, '-')
: isLegacyBuild ? 'legacy-' : ''
config.plugins.push(new BundleAnalyzerPlugin({
logLevel: 'warn',
openAnalyzer: false,
analyzerMode: args.report ? 'static' : 'disabled',
reportFilename: `${bundleName}report.html`,
statsFilename: `${bundleName}report.json`,
generateStatsFile: !!args['report-json']
}))
})
}
// 清空上一次打包
if (args.clean) {
await fs.emptyDir(targetDir)
}
return new Promise((resolve, reject) => {
webpack(webpackConfig, (err, stats) => {
stopSpinner(false)
if (err) {
return reject(err)
}
// 失败捕获
if (stats.hasErrors()) {
return reject(new Error('Build failed with errors.'))
}
log(formatStats(stats, targetDirShort, api))
if (args.target === 'app' && !isLegacyBuild) {
if (!args.watch) {
done(`Build complete. The ${chalk.cyan(targetDirShort)} directory
is ready to be deployed.`)
info(`Check out deployment
instructions at ${chalk.cyan(`https://cli.vuejs.org/guide/
deployment.html`)}\n`)
} else {
// 打包结束
done(`Build complete. Watching for changes...`)
}
}
}
// test-only signal
if (process.env.VUE_CLI_TEST) {
console.log('Build complete.')
}
resolve()
})
})
}
总结
vue-cli
是我们在快速开发的时候一大利器,官方提供了多种预设,不需要我们去配置各种依赖,是非常方便的。本文讲解了vue-cli如何去构建一个项目,他的vue-cli-serve
如何去启动一个本地服务,和怎么打包构建。通过学习了本篇,我相信各位能够在vue-cli
的基础上,去添加一些实际项目中所需要的配置包,更加快速开发。
转载自:https://juejin.cn/post/7201020302905720888