探索Vue-CLI 源码
ps:建议配合源码看、基于@vue/cli 4.5.12
当我们看到vue-cli-service serve和vue-cli-service build,不想知道是发生什么事吗?
查看node_modules\@vue\cli-service\package.json
"bin": {
"vue-cli-service": "bin/vue-cli-service.js"
},
这部分代码的作用是:当我安装@vue/cli的时候会注册一个vue-cli-service这个命令,也就是我们可以在控制台输入vue-cli-service,会调用bin/vue-cli-service.js,其中后面的参数也会传到bin/vue-cli-service.js这里面
// bin/vue-cli-service.js
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
...
...
const command = args._[0]
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
Service 是一个类,还定义一个变量command(这也就是vue-cli-service serve中的serve参数),它会调用Service类中的run方法
// ../lib/Service.js
async run (name, args = {}, rawArgv = []) {
...
this.init(mode)
...
let command = this.commands[name]
...
...
const { fn } = command
return fn(args, rawArgv)
}
我们看到有两个关键的地方:this.init(mode)和 let command = this.commands[name] ,下面会说到这有什么用
// ../lib/Service.js
module.exports = class Service {
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
...
// If there are inline plugins, they will be used instead of those
// found in package.json.
// When useBuiltIn === false, built-in plugins are disabled. This is mostly
// for testing.
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
...
}
当Service类被实例化时,会用这么一句this.plugins = this.resolvePlugins(plugins, useBuiltIn),我们看看这个方法实现了什么
// ../lib/Service.js
resolvePlugins (inlinePlugins, useBuiltIn) {
const idToPlugin = id => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(id)
})
let plugins
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// config plugins are order sensitive
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app'
].map(idToPlugin)
...
...
return plugins
}
这个最终会返回一个对象数组,里面包括了builtInPlugins里的数据(因为做了一些处理),每条数据都是一个JSON对象,其中apply这个key里面存的是require('./xxx/xxxx'),它会将每个文件引进来。我们只对./commands/serve做解释。
// ../lib/Service.js
init (mode = process.env.VUE_CLI_MODE) {
...
// apply plugins.
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
...
}
我们再看this.init(),由此可以看到,init的时候它会遍历this.plugins这个数组,也就是上面this.plugins = this.resolvePlugins(plugins, useBuiltIn)这句结果,它会进行遍历并且将apply给结构出来并调用,其实apply()就是我require('./commands/serve')(举个例子) 再调用它。所以我们接下来要看./commands/serve。
// ../lib/commands/serve.js
module.exports = (api, options) => {
api.registerCommand('serve', {
...
}, async function serve (args) {
...
})
我们可以看到,serve.js接收两个参数,也就是说apply(new PluginAPI(id, this), this.projectOptions)里面 new PluginAPI(id, this) 等于 api,api.registerCommand即调用new PluginAPI().registerCommand(),接下来看PluginAPI.js
// ../lib/PluginAPI.js
class PluginAPI {
constructor (id, service) {
this.id = id
this.service = service // 此时this.service 就是 Service 的实例
}
...
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {}}
}
...
}
在../lib/commands/serve.js 中 api.registerCommand()就是调用这里的registerCommand方法,name就是serve,opts就是一些参数,fn是定义的方法,
最终:this.service.commands['serve'] = { fn, opts: opts || {}}
// ../lib/Service.js
async run (name, args = {}, rawArgv = []) {
...
this.init(mode)
...
let command = this.commands[name]
...
...
const { fn } = command
return fn(args, rawArgv)
}
以name = 'serve'为例,所以fn就是../lib/commands/serve.js里的async function serve (args)方法
到此为止我们知道了vue-cli-serve run serve 调用的方法就是 ../lib/commands/serve.js里的async function serve (args)方法
快完结了
// ../lib/commands/serve.js
async function serve (args){
...
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
...
const webpackConfig = api.resolveWebpackConfig()
...
const compiler = webpack(webpackConfig) // 根据配置通过webpack 得到 编译后结果
...
const server = new WebpackDevServer(compiler,Object.assign{ // 将结果给到WebpackDevServer 起一个服务
...
})
server.listen(port, host, err => {
if (err) {
reject(err)
}
})
}
我们可以看到 vue-cli 最终还是会通过 webpack 进行处理,有一点要讲的是 const webpackConfig = api.resolveWebpackConfig() 它是加载webpack的配置,我们上门说到 **api **其实就是 PluginAPI,我们接着看
// ../lib/PluginAPI.js
resolveWebpackConfig (chainableConfig) {
return this.service.resolveWebpackConfig(chainableConfig)
}
// this.service 这里指的是Service 因为上面说到的Service 实例 PluginAPI 这个类时 也把自己传给PluginAPI构造器
// ../lib/Service.js
resolveChainableWebpackConfig () {
const chainableConfig = new Config()
// apply chains
this.webpackChainFns.forEach(fn => fn(chainableConfig))
return chainableConfig
}
resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
...
// get raw config
let config = chainableConfig.toConfig()
...
// 对config做一些操作
...
return config
}
我们看到resolveWebpackConfig方法调用时默认会调用resolveChainableWebpackConfig 方法,在看resolveChainableWebpackConfig方法里面有一句this.webpackChainFns.forEach(fn => fn(chainableConfig))这个webpackChainFns是怎么来的呢?
// ../lib/Service.js
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// config plugins are order sensitive
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app'
].map(idToPlugin)
还记得这个吗,我们只讲了./commands/serve里的方法,我们再看看./config/base里面
// ../lib/config/base.js
module.exports = (api, options) => {
api.chainWebpack(webpackConfig => {
...
...
webpackConfig
.mode('development')
.context(api.service.context)
.entry('app')
.add('./src/main.js')
.end()
.output
.path(api.resolve(options.outputDir))
.filename(isLegacyBundle ? '[name]-legacy.js' : '[name].js')
.publicPath(options.publicPath)
webpackConfig.resolve
.extensions
.merge(['.mjs', '.js', '.jsx', '.vue', '.json', '.wasm'])
.end()
.modules
.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'))
.end()
.alias
.set('@', api.resolve('src'))
.set(
'vue$',
options.runtimeCompiler
? 'vue/dist/vue.esm.js'
: 'vue/dist/vue.runtime.esm.js'
)
webpackConfig.resolveLoader
.modules
.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'))
webpackConfig.module
.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
// js is handled by cli-plugin-babel ---------------------------------------
// vue-loader --------------------------------------------------------------
const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
'vue-loader': require('vue-loader/package.json').version,
/* eslint-disable-next-line node/no-extraneous-require */
'@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version,
'vue-template-compiler': require('vue-template-compiler/package.json').version
})
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('cache-loader')
.loader('cache-loader')
.options(vueLoaderCacheConfig)
.end()
.use('vue-loader')
.loader('vue-loader')
.options(Object.assign({
compilerOptions: {
preserveWhitespace: false
}
}, vueLoaderCacheConfig))
webpackConfig
.plugin('vue-loader')
.use(require('vue-loader/lib/plugin'))
// static assets -----------------------------------------------------------
webpackConfig.module
.rule('images')
.test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.options(genUrlLoaderOptions('img'))
// do not base64-inline SVGs.
// https://github.com/facebookincubator/create-react-app/pull/1180
webpackConfig.module
.rule('svg')
.test(/\.(svg)(\?.*)?$/)
.use('file-loader')
.loader('file-loader')
.options({
name: genAssetSubPath('img')
})
webpackConfig.module
.rule('media')
.test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.options(genUrlLoaderOptions('media'))
webpackConfig.module
.rule('fonts')
.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
.use('url-loader')
.loader('url-loader')
.options(genUrlLoaderOptions('fonts'))
// Other common pre-processors ---------------------------------------------
webpackConfig.module
.rule('pug')
.test(/\.pug$/)
.oneOf('pug-vue')
.resourceQuery(/vue/)
.use('pug-plain-loader')
.loader('pug-plain-loader')
.end()
.end()
.oneOf('pug-template')
.use('raw')
.loader('raw-loader')
.end()
.use('pug-plain')
.loader('pug-plain-loader')
.end()
.end()
// shims
webpackConfig.node
.merge({
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
})
const resolveClientEnv = require('../util/resolveClientEnv')
webpackConfig
.plugin('define')
.use(require('webpack/lib/DefinePlugin'), [
resolveClientEnv(options)
])
webpackConfig
.plugin('case-sensitive-paths')
.use(require('case-sensitive-paths-webpack-plugin'))
// friendly error plugin displays very confusing errors when webpack
// fails to resolve a loader, so we provide custom handlers to improve it
const { transformer, formatter } = require('../util/resolveLoaderError')
webpackConfig
.plugin('friendly-errors')
.use(require('@soda/friendly-errors-webpack-plugin'), [{
additionalTransformers: [transformer],
additionalFormatters: [formatter]
}])
})
}
我们可以看到这像不像我们一开始用webpack搭建vue环境一样,vue-cli帮我们配置了很多loader之类的,我们看api.chainWebpack,api 老朋友了,我们去看PluginAPI.js
// ../lib/Service.js
chainWebpack (fn) {
this.service.webpackChainFns.push(fn)
}
由此可以看到 webpackConfig => { ... ....} 这个方法存到了 this.service.webpackChainFns这个数组里,也就是Service里的webpackChainFns数组,好我们再看回Service.js
// ../lib/Service.js
resolveChainableWebpackConfig () {
const chainableConfig = new Config()
// apply chains
this.webpackChainFns.forEach(fn => fn(chainableConfig))
return chainableConfig
}
所以, this.webpackChainFns存的是../lib/config/base.js里的方法,并把chainableConfig传进去经过处理返回回来new Config()就是new 了webpack-chain这个插件。vue.config.js在loadUserOptions方法解析,可以自己去看看。
到此为止只是粗略的讲一个流程,其实看起来比较绕,还有一些数据处理比较复杂,但是逻辑其实是清晰明了的。