什么是vue-cli脚手架
vue-cli是Vue官方提供的一个脚手架,他能够帮助开发者快速搭建一个基于vue框架的前端项目。
开发者通过npm install -g @vue/cli
可以将vue-cli安装到本地,然后通过vue create <projectName>
去初始化一个前端项目,该项目基于webpack进行构建,并可以手动选择是否在项目中安装vuex
,vueRouter
,typeScript
,esLint
...等配置。
什么是vue-cli插件
上面说到vue-cli
可以快速帮助我们去构建一个前端项目,但是他只能去默认安装一些公共的vue依赖包进去,对于定制化是不足的。在日常开发中,每个团队都会沉淀一些公用的业务代码。当我们初始化项目时,如果希望将这些业务代码自动安装的新项目去时,vue-cli
默认是做不到。
这是vue-cli插件
就可以解决这个痛点,vue-cli
可以通过vue add
,vue invoke
命令去通过安装插件的形式,将我们的自定义代码安装到我们的前端工程中去。
如何开发vue-cli插件
简单来说,就是我们需要暴露一个函数去给vue-cli
调用,当我们执行vue add
命令时候。vue-cli
就会去调用指定路径(generator/index
)文件。vue-cli
在调用的过程中,会传入一个GeneratorAPI
对象实例,这个对象实例拥有很多方法(如render
、extendPackage
,injectImports
)等,我们在函数中可以调用该实例的方法对前端项目就行修改。
vue-cli插件开发指南详细的说明了如何去开发一个vue-cli插件
,不熟悉的同学可以去看一下,本文由于是做源码解析,对如何开发不多赘述。
案例展示
module.exports = (api) => {
// 渲染template模板
api.render('./template');
// 入口文件插入两条Import语句
api.injectImports(api.entryFile, [
'import \'./api\';',
'import \'./plugins/axios\';',
]);
// 修改依赖包
api.extendPackage({
dependencies: {
axios: '^0.19.2',
'vue-axios': '^2.1.5',
},
// 修改vue.config.js配置
vue: {
devServer: {
proxy: {
'^/api': {
target: 'https://api.example.com',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
},
},
},
});
};
当我们执行vue add axios
命令(私库),就会执行以下操作
- 渲染template下的文件到我们执行
vue add
的项目下 - 在项目的入口文件插入两条import语句,引入
./api
,./plugins/axios
- 修改package.json添加axios/vue-axios依赖
- 修改vue.config.js,将devServer配置信息写入
前置的一些知识点
node的process.cwd()
返回的是当前执行上下文,也就是我们需要安装插件的项目路径。插件中如果访问相对路径就是访问的插件包里的文件。如果修改项目,则一版使用path.resolve(process.cwd(), path)
来访问项目路径。
文件路径
上面介绍了什么是vue-cli插件,以及为什么需要vue-cli插件。但当我们执行vue add <packageName>
的时候,具体做了什么呢?
如果你已经将vue-cli安装到了本地,不妨去你本地的node仓库里去查看一下vue-cli的源代码。可以在命令行工具中执行where vue
,就可以看到具体的安装目录了
vue.cmd是一个bin命令软链,具体的包是放在node_modules下
使用编辑器打开该项目,通过package.json查看包的具体信息就会发现,他的bin指向了入口文件bin/vue.js
源码解读部分
带着这个案例的操作,我们进入源码阅读环境。上面已经说得到了源码的路径,可以直接进入@vue/cli目录下,查看package.json,阅读包信息
可以看到包的命令放在bin/vue.js
文件下,接着去阅读vue.js
文件。可以看到vue-cli
的所有命令都在这个文件里面定义,他是依赖commander
这个包来进行命令的创建,commander文档。因为我们这里只用到了add
命令,所以只需要看这里
add
接收一个必填项<plugin>
也就是插件名称,和一个可选项[pluginOptions]
也就是插件配置。可以从description看到这个命令的介绍:install a plugin and invoke its generator in an already created project(安装一个插件并在已经创建的项目中调用它的generator)。通过action函数可以发现,这个命令会去执行../lib/add
这个文件下的代码。
/**
*
* @param {*} pluginToAdd 插件名称
* @param {*} options 插件配置参数
* @param {*} context 执行上下文环境(默认就是我们执行命令的项目路径)
* @returns
*/
async function add (pluginToAdd, options = {}, context = process.cwd()) {
//判断环境 忽略
if (!(await confirmIfGitDirty(context))) {
return
}
// for `vue add` command in 3.x projects
// 3.x版本兼容 已经router/vuex插件安装 自定义插件可忽略
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)
}
}
const pluginRe = /^(@?[^@]+)(?:@(.+))?$/
//拿到pluginName/pluginVersion等参数
const [
// eslint-disable-next-line
_skip,
pluginName,
pluginVersion
] = pluginToAdd.match(pluginRe)
const packageName = resolvePluginId(pluginName)
log()
log(`📦 Installing ${chalk.cyan(packageName)}...`)
log()
// 此处实例化PackageManager对象
const pm = new PackageManager({ context })
// 如果指定了版本plugin@verison
if (pluginVersion) {
await pm.add(`${packageName}@${pluginVersion}`)
// 如果是vue-cli提供的插件
} else if (isOfficialPlugin(packageName)) {
const { latestMinor } = await getVersions()
await pm.add(`${packageName}@~${latestMinor}`)
// 安装自定义插件 我们就是走这一条判断去安装
} else {
await pm.add(packageName, { tilde: true })
}
log(`${chalk.green('✔')} Successfully installed plugin: ${chalk.cyan(packageName)}`)
log()
// 去获取generator路径
const generatorPath = resolveModule(`${packageName}/generator`, context)
if (generatorPath) {
invoke(pluginName, options, context)
} else {
log(`Plugin ${packageName} does not have a generator to invoke`)
}
}
add函数主要依赖了PackageManager
这个对象,PackageManager
主要是对依赖的安装,去查看PackageManager
会发现他会对当前项目的安装工具进行判断,支持pnpm,npm,yarn
等。执行pm.add
操作就回去拉取我们插件的代码包。
const PACKAGE_MANAGER_CONFIG = {
npm: {
install: ['install', '--loglevel', 'error'],
add: ['install', '--loglevel', 'error'],
upgrade: ['update', '--loglevel', 'error'],
remove: ['uninstall', '--loglevel', 'error']
},
pnpm: hasPnpmVersionOrLater('4.0.0') ? PACKAGE_MANAGER_PNPM4_CONFIG : PACKAGE_MANAGER_PNPM3_CONFIG,
yarn: {
install: [],
add: ['add'],
upgrade: ['upgrade'],
remove: ['remove']
}
}
async runCommand (command, args) {
const prevNodeEnv = process.env.NODE_ENV
// In the use case of Vue CLI, when installing dependencies,
// the `NODE_ENV` environment variable does no good;
// it only confuses users by skipping dev deps (when set to `production`).
delete process.env.NODE_ENV
await this.setRegistryEnvs()
await executeCommand(
// this.bin只的是当前的安装工具
this.bin,
[
...PACKAGE_MANAGER_CONFIG[this.bin][command],
...(args || [])
],
this.context
)
if (prevNodeEnv) {
process.env.NODE_ENV = prevNodeEnv
}
}
async add (packageName, {
tilde = false,
dev = true
} = {}) {
const args = dev ? ['-D'] : []
if (tilde) {
if (this.bin === 'yarn') {
args.push('--tilde')
} else {
process.env.npm_config_save_prefix = '~'
}
}
if (this.needsPeerDepsFix) {
args.push('--legacy-peer-deps')
}
// 运行脚本 安装依赖
return await this.runCommand('add', [packageName, ...args])
}
执行完以上操作,我们的插件代码已经被安装到了node_modules
下,并且package.json
也会记录包信息。