vue-loader实现
使用
安装vue-loader以及vue-template-compiler:
npm install -D vue-loader vue-template-compilerwebpack配置:
// webpack.config.js 文件
const { VueLoaderPlugin } = require('vue-loader')
const config = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
],
}VueLoaderPlugin应用
找到vue-loader/lib/plugin-webpack5.js文件:
class VueLoaderPlugin {
apply (compiler) {
// ...
// 1. 定义 picther loader
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
if (!query) { return false }
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// 2. 替换 rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}该插件的主要作用是添加了一个pitcher的loader。该loader通过resourceQuery判断query是否存在,也就是文件请求路径是否带有查询参数。如果有查询参数,还需要解析query判断查询参数中是否有vue。因此,pitcher loader是在文件请求路径查询参数中包含vue时才会匹配。
解析 loaders
在make阶段的factorizeModule过程中,会进行文件的解析(resolve)。对于以.vue结尾的文件,根据rules规则,会匹配到vue-loader,从而解析出vue-loader的实际引用路径,类似如下:
'/Users/xxx/xxx/node_modules/vue-loader/lib/index.js'loader-runner
在make阶段的buildModule过程中,webpack使用loader-runner库执行loaders。对于以.vue结尾的文件,此时只会执行vue-loader。
vue-loader
找到node_modules/vue-loader/lib/index.js文件:
module.exports = function (source) {
// 1. 解析查询参数
const loaderContext = this
const { resourceQuery = ''} = loaderContext
const rawQuery = resourceQuery.slice(1)
const incomingQuery = qs.parse(rawQuery)
// 2. 解析源码
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// 3. 如果参数中有 type ,使用对应的 loader 执行
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// 4. 其他,根据不同的 type,分别生成不同的代码
}vue-loader会通过component-compiler-utils包对.vue文件代码解析,解析出来的格式为:
{
script: {
type: 'script'
// ...
},
styles: [],
template: {},
customBlocks: [],
errors: []
}在首次加载时,没有查询参数,所以会进入到第四步,根据不同的type将解析出来的代码按照模块的形式引入,如:
// type 为 template
'import { render, staticRenderFns } from "./component.vue?vue&type=template&id=a8ea6a18&scoped=true&"'
// type 为 script
'import script from "./component.vue?vue&type=script&lang=js&"\n
export * from "./component.vue?vue&type=script&lang=js&"'
// type 为 style
'import style0 from "./component.vue?vue&type=style&index=0&id=a8ea6a18&lang=css&scoped=true&"\n'pitch
vue-loader代码转换后,下次加载这些模块的时候,由于请求路径参数带有vue,因此在VueLoaderPlugin插件中添加的pitcher loader也会匹配。所以对于?vue&type=xxx结尾的文件,会匹配[pitcher loader, vue-loader]这两个loader。首先执行的是pitcher loader 的pitch方法:
if (query.type === `template`) {
const request = genRequest([
...cacheLoader,
...postLoaders,
templateLoaderPath + `??vue-loader-options`,
...preLoaders
])
return `export * from ${request}`
}该函数同样是根据不同的type对引用路径做不同的处理。比如template在加载时,由于还需要解析template内的代码,所以加入了templateLoader:
const { compileTemplate } = require('@vue/component-compiler-utils')
const compiled = compileTemplate(finalOptions)
const { code } = compiled
return code + `\nexport { render, staticRenderFns }`该loader会将template中Vue代码进行编译。(这里template中的代码则是由vue-loader中的selectBlock方法提供)
vue热更新
在vue-loader/lib/index.js文件中后半段,处理完script/template/style等代码之后,会添加热更新相关代码:
// 整理属性,定义 component 变量
`/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
${hasScoped ? JSON.stringify(id) : `null`},
${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ``}
)
`.trim() + `\n`
// 添加 module.hot 代码
if (needsHotReload) {
code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
}首先定义了component变量,随后通过genHotReloadCode方法添加热更新代码。
genHotReloadCode
在lib/codegen/hotReload.js文件中找到genHotReloadCode方法:
const hotReloadAPIPath = JSON.stringify(require.resolve('vue-hot-reload-api'))
exports.genHotReloadCode = (id, functional, templateRequest) => {
return `
/* hot reload */
if (module.hot) {
var api = require(${hotReloadAPIPath})
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('${id}')) {
api.createRecord('${id}', component.options)
} else {
api.${functional ? 'rerender' : 'reload'}('${id}', component.options)
}
${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''}
}
}
`.trim()
}api.install
如果module.hot存在,首先会加载vue-hot-reload-api模块,然后调用其install方法:
exports.install = function (vue, browserify) {
if (installed) { return }
installed = true
Vue = vue.__esModule ? vue.default : vue
version = Vue.version.split('.').map(Number)
exports.compatible = version[0] >= 2
}主要是处理一些版本兼容的问题,如vue-hot-reload-api只适合Vue版本大于2的情况。
api.createRecord
其次如果id没有被记录的话,会执行api.createRecord('${id}', component.options)方法:
exports.createRecord = function (id, options) {
if(map[id]) { return }
// 1. Ctor 为当前组件的构造函数
var Ctor = null
if (typeof options === 'function') {
Ctor = options
options = Ctor.options
}
// 2. 注入生命周期
makeOptionsHot(id, options)
// 3. 记录当前id 对应的构造函数等信息
map[id] = {
Ctor: Ctor,
options: options,
instances: []
}
}其中makeOptionsHot方法在不为函数组件时,会执行下列代码:
// 注入 beforeCreate 生命周期回调
injectHook(options, initHookName, function() {
var record = map[id]
if (!record.Ctor) {
record.Ctor = this.constructor
}
record.instances.push(this)
})
// 注入 beforeDestroy 生命周期回调
injectHook(options, 'beforeDestroy', function() {
var instances = map[id].instances
instances.splice(instances.indexOf(this), 1)
})当注入了beforeCreate后,组件被实例化时,就会将组件实例添加到instances中。
api.reload
当前自身模块发生变化时,如果该component已经被record了,那么会执行api.reload方法:
// 继承新的 options
var newCtor = record.Ctor.super.extend(options)
newCtor.options._Ctor = record.options._Ctor
record.Ctor.options = newCtor.options
record.Ctor.cid = newCtor.cid
record.Ctor.prototype = newCtor.prototype
record.instances.slice().forEach(function (instance) {
if (instance.$vnode && instance.$vnode.context) {
instance.$vnode.context.$forceUpdate()
}
}该函数会替换原来记录的组件构造函数,并遍历相关实例进行更新。
api.rerender
最后执行genTemplateHotReloadCode方法:
const genTemplateHotReloadCode = (id, request) => {
return `
module.hot.accept(${request}, function () {
api.rerender('${id}', {
render: render,
staticRenderFns: staticRenderFns
})
})
`.trim()
}该方法通过module.hot.accept监听组件中template的变化。如果template有变化,那么执行api.rerender方法:
exports.rerender = tryWrap(function (id, options) {
var record = map[id]
if (record.Ctor) {
record.Ctor.options.render = options.render
record.Ctor.options.staticRenderFns = options.staticRenderFns
record.instances.slice().forEach(function (instance) {
instance.$options.render = options.render
instance.$options.staticRenderFns = options.staticRenderFns
var restore = patchScopedSlots(instance)
instance.$forceUpdate()
instance.$nextTick(restore)
})
}
}此时会遍历收集到的instances,通过$forceUpdate方法强制更新组件。
总结
vue-loader的实现主要包括以下几个步骤:
- 通过
VueLoaderPlugin替换webpack中module.rules配置,添加一个pitcher loader,该loader会匹配路径参数中带有vue的文件。 - 通过
loader-runner对.vue文件解析。通过component-compiler-utils包将.vue文件解析为对象形式,然后根据不同的type(script/template等等),生成不同的查询参数,添加到.vue文件后。 - 当下一次
build时,遇到前面处理后带参数的.vue文件,会匹配到VueLoaderPlugin插件中添加的pitcher loader。首先会执行pitcher loader的pitch方法,该方法根据参数中不同的type为文件添加不同的loader进行处理。如template会添加templateLoader,css添加css-loader等。相当于根据路径的type为路径包装了一层。添加完成后,会移除pitcher loader。 - 在下一次进入到
vue-loader时,由于存在type,此时进入到selectBlock环节。根据不同的type对相应代码做不同的处理即可。
vue-loader热更新实现原理:
- 通过
vue-loader解析代码的末尾,将解析出来的属性整理成component对象,并添加热更新代码。 - 热更新代码首先调用
createRecord会为当前component注入beforeCreate等生命周期。当组件实例化时,会记录下组件实例。 - 如果当前模块代码改变了,且
record存在,此时调用reload方法更新组件构造函数,并重新渲染。 - 如果
vue组件中template代码改变了,此时调用rerender方法进行重新渲染。
