Vue SFC(单文件组件)有着特殊的结构,要使得webpack可以正确的加载SFC,就需要借助loader,vue官方的loader就是vue-loader,
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
最近简单学习了其大致的工作原理,其中也有不少没完全弄懂的地方,但是先简单记录下。
vue-loader 运行过程大致上可以划分为两个阶段:
- 预处理阶段:动态修改 Webpack 配置,注入 vue-loader 专用的一系列 module.rules;
- 内容处理阶段:Normal Loader 配合 Pitch Loader 完成文件内容转译
简单的整体描述下原理,vue-loader的核心目的其实是将SFC组件的各个模块拆开,然后对各个模块复用用户自己期望的配置,比如对script,用户可能配置了用babel解析js,对style,用户可能配置了less/css/style这些loader。
阶段一:预处理,是借助插件,重写用户的webpack配置,动态增加了pitchloader,复制用户的rules,插入到rules中。这些新的rules可以被后续的处理中间过程产物命中。
阶段二:先通过vue-loader(第一次)对import vue这种语句进行转换,转换之后带上vue标记和type标记,type标记区别了各个SFC的模块,而带上vue标记之后,就可以被pitch-loader命中;pitch-loader进一步处理这些新的import语句,用行内rules对不同的type定制不同的rules,但是都会定制到vue-loader。
第二次走vue-loader,因为有vue标记了,vue-loader就会提取并返回不同模块的资源,然后后续的行内样式都是复用用户自定义的,也就是资源可以被正确的处理了。
(另外,上面描述的pitch和loader的pitch阶段不是一回事,这里是一个loader,其名称叫pitch-loader)
预处理
vue-loader 插件会在 apply 函数中动态修改 Webpack 配置。
插件主要完成两个任务:
- 初始化并注册 Pitch Loader:定义pitcher对象,指定loader路径为 require.resolve(‘./loaders/pitcher’) ,并将pitcher注入到 rules 数组首位。这种动态注入的好处是用户不用关注 —— 不去看源码根本不知道还有一个pitcher loader,而且能保证pitcher能在其他rule之前执行,确保运行顺序。
- 复制 rules 配置:代码第8行遍历 compiler.options.module.rules 数组,也就是用户提供的 Webpack 配置中的 module.rules 项,对每个rule执行 cloneRule 方法复制规则对象。
之后,将 Webpack 配置修改为 [pitcher, …clonedRules, …rules],cloneRules从开发者提供的配置中复制过来的,内容相似,只是 cloneRule 在复制过程会给这些规则重新定义 resourceQuery。
内容处理
- 路径命中 /.vue$/i 规则,调用 vue-loader 生成中间结果 A;
- 结果 A 命中 xx.vue?vue 规则,调用 vue-loader Pitch Loader 生成中间结果 B;
- 结果 B 命中具体 Loader,直接调用 Loader 做处理。 ```javascript // 原始代码 import xx from ‘./index.vue’;
// 第一步,命中 vue-loader,转换为: import { render, staticRenderFns } from “./index.vue?vue&type=template&id=2964abc9&scoped=true&” import script from “./index.vue?vue&type=script&lang=js&” export * from “./index.vue?vue&type=script&lang=js&” import style0 from “./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&”
// 第二步,命中 pitcher,转换为: export from “-!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=template&id=2964abc9&scoped=true&” import mod from “-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&”; export default mod; export from “-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&” export * from “-!../../node_modules/mini-css-extract-plugin/dist/loader.js!../../node_modules/css-loader/dist/cjs.js!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&”
// 第三步,根据行内路径规则按序调用loader
比如对于的代码: index.vue:
```vue
<template>
<div class="root">hello world</div>
</template>
<script>
export default {
data() {},
mounted() {
console.log("hello world");
},
};
</script>
<style scoped>
.root {
font-size: 12px;
}
</style>
- 第一次执行 vue-loader ,执行如下逻辑:
- 调用 @vue/component-compiler-utils 包的parse函数,将SFC 文本解析为AST对象;
- 遍历 AST 对象属性,转换为特殊的引用路径;
- 返回转换结果。
第一次转换结果:
import { render, staticRenderFns } from "./index.vue?vue&type=template&id=2964abc9&scoped=true&"
import script from "./index.vue?vue&type=script&lang=js&"
export * from "./index.vue?vue&type=script&lang=js&"
import style0 from "./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"2964abc9",
null
)
...
export default component.exports
这里并没有真的处理 block 里面的内容,而是简单地针对不同类型的内容块生成 import 语句:
- Script:”./index.vue?vue&type=script&lang=js&”
- Template: “./index.vue?vue&type=template&id=2964abc9&scoped=true&”
- Style: “./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&”
这些路径都对应原始的 .vue 路径基础上增加了 vue 标志符及 type、lang 等参数。这个vue标志是可以被上文提到的resourceQuery命中到的。
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
if (!query) { return false }
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
}
}
命中 xx.vue?vue 格式的路径,也就是说上面 vue-loader 转换后的 import 路径会被 Pitch Loader 命中,做进一步处理.
Pitch Loader 的逻辑比较简单,做的事情也只是转换 import 路径。处理后,会得到一个新的行内路径:
import mod from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&";
之后 Webpack 会按照下述逻辑运行:
调用 vue-loader 处理 index.js 文件;
- 调用 babel-loader 处理上一步返回的内容。
给了 vue-loader 第二次执行的机会。第二次运行时由于路径已经带上了 type 参数,会命中上面第26行的判断语句,进入 selectBlock 函数。
module.exports = function selectBlock (
descriptor,
loaderContext,
query,
appendExtension
) {
// template
if (query.type === `template`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
}
loaderContext.callback(
null,
descriptor.template.content,
descriptor.template.map
)
return
}
// script
if (query.type === `script`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
}
loaderContext.callback(
null,
descriptor.script.content,
descriptor.script.map
)
return
}
// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[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[query.index]
loaderContext.callback(
null,
block.content,
block.map
)
return
}
}
至此,就可以完成从 Vue SFC 文件中抽取特定 Block 内容,并复用用户定义的其它 Loader 加载这些 Block。