浅析vue-loader
环境配置
// webpack.config.js
const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
devtool: false,
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
hot: true,
open: true,
},
module: {
rules: [
{ test: /\.vue$/, use: ["vue-loader"] },
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: 'index.html'
}),
],
};
// index.js
import { createApp } from 'vue'
import App from "./App.vue";
const app = createApp(App)
app.mount('#app')
代码调试
VueLoaderPlugin
ruleSetCompiler && BasicMatcherRulePlugin
const ruleSetCompiler = new RuleSetCompiler([
new BasicMatcherRulePlugin('test', 'resource'),
new BasicMatcherRulePlugin('mimetype'),
new BasicMatcherRulePlugin('dependency'),
new BasicMatcherRulePlugin('include', 'resource'),
new BasicMatcherRulePlugin('exclude', 'resource', true),
new BasicMatcherRulePlugin('conditions'),
new BasicMatcherRulePlugin('resource'),
new BasicMatcherRulePlugin('resourceQuery'),
new BasicMatcherRulePlugin('resourceFragment'),
new BasicMatcherRulePlugin('realResource'),
new BasicMatcherRulePlugin('issuer'),
new BasicMatcherRulePlugin('compiler'),
...objectMatcherRulePlugins,
new BasicEffectRulePlugin('type'),
new BasicEffectRulePlugin('sideEffects'),
new BasicEffectRulePlugin('parser'),
new BasicEffectRulePlugin('resolve'),
new BasicEffectRulePlugin('generator'),
new UseEffectRulePlugin(),
]);
BasicEffectRulePlugin
主要是对rule的改造,它删除了rule中的"test","mimetype"等等属性,并增加了conditions
RuleSetCompiler
创建了hooks.rule这个SyncHook
,然后后面match
的时候会call这个hook,由于vue-loader是第一个,所以它最先进入match
,最后vue-loader的rule的处理结果为
test变为了一个函数放在conditions
之中,use放在了effects
之中
function match(rule, fakeFile) {
let ruleSet = matcherCache.get(rule);
if (!ruleSet) {
// skip the `include` check when locating the vue rule
const clonedRawRule = Object.assign({}, rule);
delete clonedRawRule.include;
ruleSet = ruleSetCompiler.compile([clonedRawRule]);
matcherCache.set(rule, ruleSet);
}
return ruleSet.exec({
resource: fakeFile,
});
}
比如执行vueRules = match(rawRule, 'foo.vue');
会进入上文处理过的rule,然后conditions
判断为true,就返回对应的loader
NormalModuleFactory
在NormalModuleFactory
这个生命周期中,就会进行上述ruleSetCompiler
的调用
之前很奇怪给resourceQuery
打断点的时候会卡在这个生命周期,原因是matchWhenEmpty: condition("")
在生成rules的时候,给matchWhenEmpty赋值会调用condition
之后就执行compile,会创建一个NormalModuleFactory
接着html-webpack-plugin
会创建一个childCompiler,也会创建一次NormalModuleFactory
生成rules
首先VueLoaderPlugin
会对module.rule除了vue-loader的rule进行复制并在clone的过程中同样用了ruleSetCompiler
进行rule的改写,这里只有css的rule,命中条件为xx.vue?vue&lang=css
{
resource: (resources) => {
currentResource = resources
return true
},
resourceQuery: (query) => {
if (!query) {
return false
}
const parsed = qs.parse(query.slice(1))
if (parsed.vue == null) {
return false
}
if (!ruleCheck(parsed, compiledRule)) {
return false
}
const fakeResourcePath = ruleResource(parsed, currentResource)
for (const condition of compiledRule.conditions) {
// add support for resourceQuery
const request =
condition.property === 'resourceQuery' ? query : fakeResourcePath
if (condition && !condition.fn(request)) {
return false
}
}
return true
},
use: [{
loader: "style-loader",
}, {
loader: "css-loader"
}]
}
之后会创建一个pitcher-loader,命中条件为xx.vue?vue
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
}
}
还有templateCompilerRule,命中条件为xx.vue?vue&type=template
const templateCompilerRule = {
loader: require.resolve('./templateLoader'),
resourceQuery: (query) => {
if (!query) {
return false;
}
const parsed = qs.parse(query.slice(1));
return parsed.vue != null && parsed.type === 'template';
},
options: vueLoaderOptions,
};
所以最后的rules共有5个
compiler.options.module.rules = [
pitcher,
templateCompilerRule,
...clonedRules, // 拷贝的css
...rules, // vue-loader和css2个
];
这就是VueLoaderPlugin所做的工作,只是添加了几个loader
vue-loader
文件的加载顺序是:index.js->index.html->app.vue这几个都没命中对应loader,略过
首先根据入口文件import App from "./App.vue";
处理App.vue文件,命中vue-loader
source为App.vue文件的内容
这个文件会根据路径通过hash生成id
给App.vue文件的scrpit部分增加query参数,使它能够命中pitch-loader
给App.vue文件的template也同样增加query参数
样式文件也一样
经过处理,生成了三个import
import { render } from "./App.vue?vue&type=template&id=7ba5bd90"
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"
import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"
后面又加上了热更新的代码
import { render } from "./App.vue?vue&type=template&id=7ba5bd90"
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"
import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"
import exportComponent from "/Users/g/webpack-book-samples/5-1_use-vue/node_modules/vue-loader/dist/exportHelper.js"
const __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__file',"src/App.vue"]])
/* hot reload */
if (module.hot) {
__exports__.__hmrId = "7ba5bd90"
const api = __VUE_HMR_RUNTIME__
module.hot.accept()
if (!api.createRecord('7ba5bd90', __exports__)) {
api.reload('7ba5bd90', __exports__)
}
module.hot.accept("./App.vue?vue&type=template&id=7ba5bd90", () => {
api.rerender('7ba5bd90', render)
})
}
export default __exports__
第一次进入vue-loader就会调用sfc,存储的键值就是App.vue的内容,并且缓存下来,下次剩下两个就可以直接用缓存了
sfc的结果就是将template,script,style分开
loader命中
由于这三个import的query都带有vue,所以都会进入pitch-loader
按照代码顺序,首先处理template
template import会进入专门处理template的template-loader
后面进行其他rule的校验,由于此时webpack只配置了css的校验,所以只有css的import才会进入clonedRules
- template -〉 pitch-loader -〉template-loader -〉vue-loader
- script -〉 pitch-loader -〉vue-loader
- css -〉 pitch-loader-〉css-loader -〉style-loader -〉vue-loader
pitch-loader
有一个很重要的一点是,normalModuleFactory
处理import的时候,并不是同步的!!!这就是import和require的差别;import是能够tree-shaking的,所以虽然按照顺序是template->script->style这个顺序来导入的,loader也是这么命中的,但是加载module的时候,是按照使用的顺序,若是没有引用,可以three-shaking掉,所以第一个处理的模块是script
pitch-loader只是将import转成了行内loader的写法,类似style-loader
// script
import script from "./App.vue?vue&type=script&lang=js"
export * from "./App.vue?vue&type=script&lang=js"
/* 转换成了下面的写法 */
export { default } from "-!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=script&lang=js";
export * from "-!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=script&lang=js"
// template
import { render } from "./App.vue?vue&type=template&id=7ba5bd90"
/* 转换成了下面的写法 */
export * from "-!../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[1]!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=template&id=7ba5bd90"
// style
import "./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"
/* 转换成了下面的写法 */
export * from "-!../node_modules/style-loader/dist/cjs.js!../node_modules/css-loader/dist/cjs.js!../node_modules/vue-loader/dist/stylePostLoader.js!../node_modules/vue-loader/dist/index.js??ruleSet[1].rules[3].use[0]!./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css"
注意style多了个stylePostLoader
第二次vue-loader
由于这次携带query,会进入selectBlock
function selectBlock(descriptor, scopeId, options, loaderContext, query, appendExtension) {
// template
if (query.type === `template`) {
// if we are receiving a query with type it can only come from a *.vue file
// that contains that block, so the block is guaranteed to exist.
const template = descriptor.template;
if (appendExtension) {
loaderContext.resourcePath += '.' + (template.lang || 'html');
}
loaderContext.callback(null, template.content, template.map);
return;
}
// script
if (query.type === `script`) {
const script = (0, resolveScript_1.resolveScript)(descriptor, scopeId, options, loaderContext);
if (appendExtension) {
loaderContext.resourcePath += '.' + (script.lang || 'js');
}
loaderContext.callback(null, script.content, script.map);
return;
}
// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[Number(query.index)];
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css');
}
loaderContext.callback(null, style.content, style.map);
return;
}
// custom
if (query.type === 'custom' && query.index != null) {
const block = descriptor.customBlocks[Number(query.index)];
loaderContext.callback(null, block.content, block.map);
}
}
script会将sfc对script的处理结果返回
template还会进入template-loader
返回结果是一个render函数,能渲染vnode
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h3", null, _toDisplayString($data.message), 1 /* TEXT */)
]))
}
css会进入stylePostLoader
,给带scoped字段的style文件增加data-v-[id],后续处理就和之前讲的css-loader和style-loader的步骤一样了
转载自:https://juejin.cn/post/7205208559263514680