阅读思路
看结构
根据这个结构可以看出是基于lerna创建的多包的格式。
其中,.github提供规范,_mock_提供插件测试环境,docs是通过vitepress创建的官方文档(https://cli.vuejs.org/),scripts是使用node来实现package.json当中npm script集成的对应的能力的一些文件。
看package.json
"private": true, // 说明整个大包不是真正引用的方式 "workspaces": [ "packages/@vue/*", "packages/test/*", "packages/vue-cli-version-marker" ], // 这三个包才是真正cli集成的功能
接着看devDependencies,这块是重点,要分别去npm官网去搜这些依赖项的作用。
看官网
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供:
通过
@vue/cli
实现的交互式的项目脚手架。通过
@vue/cli
+@vue/cli-service-global
实现的零配置原型开发。一个运行时依赖 (
@vue/cli-service
),该依赖:
可升级;
基于 webpack 构建,并带有合理的默认配置;
可以通过项目内的配置文件进行配置;
可以通过插件进行扩展。
一个丰富的官方插件集合,集成了前端生态中最好的工具。
一套完全图形化的创建和管理 Vue.js 项目的用户界面。
我们要读的是如何用webpack去集成的,插件是怎么去使用的。
插件和preset
看一个项目的好坏通过插件和 preset 是否完善。
如果你查阅一个新创建项目的
package.json
,就会发现依赖都是以@vue/cli-plugin-
开头的。插件可以修改 webpack 的内部配置,也可以向vue-cli-service
注入命令。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的。
可以直接去npm官网去搜@vue/cli-plugin-
开头的插件。
官网
配置指南
vue.config.js
导出一个json对象,也可以用@vue/cli-service
提供的 defineConfig
,获得更好的类型提示
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ // 选项 })
publicPath
publicPath是部署的路径,默认为'/'
,也可以设置成相对路径,以便于部署到任意路径。
outputDir
build
生成的文件目录,默认是'dist'
assetsDir
js/css/img/fonts等静态资源相对于outputDir的目录
indexPath
生成的index.html的路径,相对于outputDir
filenameHashing
在静态资源文件名上添加hash以便更好的控制缓存,默认为true
。
pages
一个对象,在multi-page模式下使用,每个page对应一个js入口文件,其值应该是一个对象,对象的 key 是入口的名字,value 是:
-
一个指定了
entry
,template
,filename
,title
和chunks
的对象 (除了entry
之外都是可选的); -
或一个指定其
entry
的字符串。
lintOnSave
是否需要每次保存时lint代码,需要安装@vue/cli-plugin-eslint
。
常用值
-
true
或'warning'
,lint警告输出到命令行,且不会编译失败 -
'default'
,lint错误输出为编译错误,编译失败 -
'error'
,lint警告输出为编译错误,编译失败
也可以通过设置让浏览器 overlay 同时显示警告和错误:
// vue.config.js
module.exports = {
devServer: {
overlay: {
warnings: true,
errors: true
}
}
}
当 lintOnSave
是一个 truthy 的值时,eslint-loader
在开发和生产构建下都会被启用。如果你想要在生产构建时禁用 eslint-loader
,可以用如下配置:
// vue.config.js
module.exports = {
lintOnSave: process.env.NODE_ENV !== 'production'
}
runtimeCompiler
是否使用包含运行时编译器的 Vue 构建版本。设置为 true
后就可以在 Vue 组件中使用 template
选项了,但是这会让应用额外增加 10kb 左右,默认为false
。
transpileDependencies
接收布尔值或一个包含字符串和正则的数组,转译 node_modules
中的第三方依赖。
productionSourceMap
生产环境的source map,默认为true
crossorigin
生成的HTML中<link rel="stylesheet">
和 <script>
标签的 crossorigin
属性。是一个字符串,参考CORS settings attributes。
integrity
在生成的 HTML 中的 <link rel="stylesheet">
和 <script>
标签上启用SRI。如果你构建后的文件是部署在 CDN 上的,启用该选项可以提供额外的安全性。
configureWebpack
如果这个值是一个对象,则会通过webpack-merge合并到最终的配置中。
如果这个值是一个函数,则会接收被解析的配置作为参数。该函数既可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。
chainWebpack
是一个函数,会接收一个基于webpack-chain的 ChainableConfig
实例。允许对内部的 webpack 配置进行更细粒度的修改。
css.requireModuleExtension
默认情况下,只有 *.module.[ext]
结尾的文件才会被视作 CSS Modules 模块。设置为 false
后可以去掉 .module
并将所有的 *.(css|scss|sass|less|styl(us)?)
文件视为 CSS Modules 模块。
css.extract
Default: 生产环境下是 true
,开发环境下是 false
是否将组件中的 CSS 提取至一个独立的 CSS 文件中 (而不是动态注入到 JavaScript 中的 inline 代码)。
css.sourceMap
是否为 CSS 开启 source map。设置为 true
之后可能会影响构建的性能。
css.loaderOptions
向 CSS 相关的 loader 传递选项。
支持的 loader 有:
-
css-loader
-
postcss-loader
-
sass-loader
-
less-loader
-
stylus-loader
另外,也可以使用 scss
选项,针对 scss
语法进行单独配置(区别于 sass
语法)。
devServer
支持所有 webpack-dev-server。
devServer.proxy
在开发环境下将API请求代理到API服务器,解决跨域问题,可以接受url字符串,也可以是一个对象。
module.exports = {
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': {
target: '<other_url>'
}
}
}
}
parallel
-
Type:
boolean
-
Default:
require('os').cpus().length > 1
是否为 Babel 或 TypeScript 使用 thread-loader
。
pwa
-
Type:
Object
向 PWA 插件传递选项。
pluginOptions
传递任何第三方插件选项
源码
packages
vue-cli-version-marker
用来标记对应版本号,可以理解成一个空包
test
测试用的包
@vue
这个包是重点
cli
脚手架基本功能
cli-init
项目初始化集成的脚本
cli-plugin-*
cli官方插件
cli-service
基于webpack构建的打包配置项
阅读思路
从vue create命令输入开始的流程入手去读。
-
执行vue create指令
-
git初始化项目
-
安装对应的cli插件
-
运行generator,创建模版
-
安装对应的依赖(node_modules)
-
调用安装之后的hooks
cli
根据官网,cli当中集成的是终端里的vue
命令
在 packages /@vue/cli 的 package.json 中可以看到我们vue create
其实是运行了/@vue/cli/bin 目录下的 vue.js (要根据报错提示安装依赖)。接下来就要看vue.js
vue.js
vue.js 使用 commander 包,这是一个主流的实现脚手架能力的包,并且最终返回的也是创建的 commander 实例。
在loadCommand中,实现了一个查找依赖的功能,具体代码如下:
try {
return require(moduleName)
} catch (err) {
if (isNotFoundError(err)) {
try {
return require('import-global')(moduleName)
} catch (err2) {
if (isNotFoundError(err2)) {
const installCommand = getGlobalInstallCommand()
console.log()
console.log(
` Command ${chalk.cyan(`vue ${commandName}`)} requires a global addon to be installed.\n` +
` Please run ${chalk.cyan(`${installCommand} ${moduleName}`)} and try again.`
)
console.log()
process.exit(1)
} else {
throw err2
}
}
} else {
throw err
}
}
可以看到首先使用require(module)尝试引入依赖,如果没有找到就用 import-global 包去找有没有全局依赖,最后报错,提示安装依赖,安装提示通过 getGlobalInstallCommand 文件判断 yarn/pnpm/npm。
接着会有一个checkNodeVersion
函数判断node版本,对应的是package.json中的engines。
"engines": { "node": "^12.0.0 || >= 14.0.0" }
然后读取对应参数的输入,进入lib下create文件中。
create.js
create.js就是根据用户的输入进行集成的能力,首先包含一些项目名是否存在,是否合法的判断,并且输出对应的报错和警告。
对于已存在的文件询问是否需要覆盖或合并,并做相应处理,完成后进入lib下的Creator.js中的create函数。
Creator.js
核心就是我们输入create之后的流程。
首先判断是否使用preset和default,如果使用了,那么就调用resolvePreset
。
然后安装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) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// legacy support for vuex
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
初始化创建:
log(`✨ Creating project in ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating' })
获取最新的cli版本
const { latestMinor } = await getVersions()
生成package.json以及devDepenencies
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
})
生成pnpm的.npmrc文件
if (packageManager === 'pnpm') {
const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
? 'shamefully-hoist=true\nstrict-peer-dependencies=false\n'
: 'shamefully-flatten=true\n'
await writeFileTree(context, {
'.npmrc': pnpmConfig
})
}
在安装依赖前要创建git仓库,以便于vue-cli-service能设置git hooks
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
log(`🗃 Initializing git repository...`)
this.emit('creation', { event: 'git-init' })
await run('git init')
}
然后安装cli-plugins
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install' })
再往后执行generator,触发create中的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
})
所以接下来要去看generator做了什么。
Generator.js
Generator会根据用户的输入去添加对应的文件,例如babel.
babel: new ConfigTransform({
file: {
js: ['babel.config.js']
}
}),
还有vue.config.js.
const reservedConfigTransforms = {
vue: new ConfigTransform({
file: {
js: ['vue.config.js']
}
})
}
其中ConfigTransform
是对应的写文件的动作,而写文件的模板集成在插件中的generator下的template当中了
cli-service
CLI 服务是构建于 webpack 和 webpack-dev-server 之上的。它包含了:
-
加载其它 CLI 插件的核心服务;
-
一个针对绝大部分应用优化过的内部的 webpack 配置;
-
项目内部的
vue-cli-service
命令,提供serve
、build
和inspect
命令。
和cli一样,cli-service指令其实是运行了bin目录下的 vue-cli-service.js
vue-cli-service.js
一样也是先判断node版本。
然后通过Service的实例去执行对应的指令(serve, build, lint)
Service.js
这里是最重要的内容。
首先进行初始化,初始化包括加载对应的环境(解读cross-env对应的配置),根据环境进行对应的配置。
const defaultNodeEnv = (mode === 'production' || mode === 'test')
? mode
: 'development'
if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
process.env.NODE_ENV = defaultNodeEnv
}
if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
process.env.BABEL_ENV = defaultNodeEnv
}
接着根据启动的参数配置插件
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// config plugins are order sensitive
'./config/base',
'./config/assets',
'./config/css',
'./config/prod',
'./config/app'
].map((id) => idToPlugin(id))
这块的内容在command文件夹下,下面以serve为例,这块代码很长。
serve.js
首先接收参数,这块对应的是vue-cli-service中的参数。
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) {
})
回到Service.js,接下来是接受用户在vue.config.js当中的配置
loadUserOptions () {
const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
if (isPromise(fileConfig)) {
return fileConfig
.then(mod => mod.default)
.then(loadedConfig => resolveUserConfig({
inlineOptions: this.inlineOptions,
pkgConfig: this.pkg.vue,
fileConfig: loadedConfig,
fileConfigPath
}))
}
return resolveUserConfig({
inlineOptions: this.inlineOptions,
pkgConfig: this.pkg.vue,
fileConfig,
fileConfigPath
})
}
可以看到使用的是loadFileConfig函数,这块的能力被集成在utils目录当中。
function loadFileConfig (context) {
let fileConfig, fileConfigPath
const possibleConfigPaths = [
process.env.VUE_CLI_SERVICE_CONFIG_PATH,
'./vue.config.js',
'./vue.config.cjs',
'./vue.config.mjs'
]
for (const p of possibleConfigPaths) {
const resolvedPath = p && path.resolve(context, p)
if (resolvedPath && fs.existsSync(resolvedPath)) {
fileConfigPath = resolvedPath
break
}
}
if (fileConfigPath) {
const { esm } = isFileEsm.sync(fileConfigPath)
if (esm) {
fileConfig = import(pathToFileURL(fileConfigPath))
} else {
fileConfig = loadModule(fileConfigPath, context)
}
}
return {
fileConfig,
fileConfigPath
}
}
这块代码不多,就是分commonjs和ESmodule来解析用户在vue.config.js/mjs/cjs当中的配置项,并且进行导入,最后返回fileConfig。
cli-plugin
按照官网的介绍,我们的插件都是以 @vue/cli-plugin-
(内建插件) 或 vue-cli-plugin-
(社区插件) 开头,列在 package.json
中的,在运行vue-cli-service
命令时,被解析的。
这部分的功能在@vue下的 cli-shared-utils 的 lib 目录中的 pluginResolution.js 当中。
pluginResolution.js
exports.resolvePluginId = id => {
// already full id
// e.g. vue-cli-plugin-foo, @vue/cli-plugin-foo, @bar/vue-cli-plugin-foo
if (pluginRE.test(id)) {
return id
}
// 如果是基本的service
if (id === '@vue/cli-service') {
return id
}
// 如果是官网的插件
if (officialPlugins.includes(id)) {
return `@vue/cli-plugin-${id}`
}
// 如果是社区的插件
if (id.charAt(0) === '@') {
const scopeMatch = id.match(scopeRE)
if (scopeMatch) {
const scope = scopeMatch[0]
const shortId = id.replace(scopeRE, '')
return `${scope}${scope === '@vue/' ? `` : `vue-`}cli-plugin-${shortId}`
}
}
// default short
// e.g. foo
return `vue-cli-plugin-${id}`
}
官网的插件包括:
const officialPlugins = [
'babel',
'e2e-cypress',
'e2e-nightwatch',
'e2e-webdriverio',
'eslint',
'pwa',
'router',
'typescript',
'unit-jest',
'unit-mocha',
'vuex',
'webpack-4'
]
下面以vuex为例看一下插件是如何集成的
cli-plugin-vuex
前面说了本质上就是将 template 中的模板注入到我们最后生成的项目文件中去。
先看入口文件index.js
index.js
module.exports = (api, options = {}, rootOptions = {}) => {
api.injectImports(api.entryFile, `import store from './store'`)
// 判断vue版本,根据版本安装插件的配置项,并且按模板注入
if (rootOptions.vueVersion === '3') {
api.transformScript(api.entryFile, require('./injectUseStore'))
api.extendPackage({
dependencies: {
vuex: '^4.0.0'
}
})
api.render('./template-vue3', {})
} else {
api.injectRootOptions(api.entryFile, `store`)
api.extendPackage({
dependencies: {
vuex: '^3.6.2'
}
})
api.render('./template', {})
}
if (api.invoking && api.hasPlugin('typescript')) {
/* eslint-disable-next-line node/no-extraneous-require */
const convertFiles = require('@vue/cli-plugin-typescript/generator/convert')
convertFiles(api)
}
}
然后通过vue add 添加到项目中,而 vue add 的能力集成在 cli/lib 下的 add.js 中
add.js
依赖模块的加载
// for `vue add` command in 3.x projects
const servicePkg = loadModule('@vue/cli-service/package.json', context)
if (servicePkg && semver.satisfies(servicePkg.version, '3.x')) {
// special internal "plugins"
if (/^(@vue\/)?router$/.test(pluginToAdd)) {
return addRouter(context)
}
if (/^(@vue\/)?vuex$/.test(pluginToAdd)) {
return addVuex(context)
}
}
接着根据插件版本进行安装
if (pluginVersion) {
await pm.add(`${packageName}@${pluginVersion}`)
} else if (isOfficialPlugin(packageName)) {
const { latestMinor } = await getVersions()
await pm.add(`${packageName}@~${latestMinor}`)
} else {
await pm.add(packageName, { tilde: true })
}
安装是通过PackageManager
类来实现的。
具体包括运行判断是否需要PeerDepsFix,判断 npm/yarn 并执行,
vue add 和 npm install 的区别
vue add 是 cli 提供的能力,他包括以下的能力
-
根据插件判断版本,会进行插件名称的转换
-
进行插件安装,包含对包管理工具的判断(根据lock文件来看)
-
生成插件中提供的模板