Vue SFC(单文件组件)有着特殊的结构,要使得webpack可以正确的加载SFC,就需要借助loader,vue官方的loader就是vue-loader,

  1. // webpack.config.js
  2. const { VueLoaderPlugin } = require('vue-loader')
  3. module.exports = {
  4. module: {
  5. rules: [
  6. // ... 其它规则
  7. {
  8. test: /\.vue$/,
  9. loader: 'vue-loader'
  10. }
  11. ]
  12. },
  13. plugins: [
  14. // 请确保引入这个插件!
  15. new VueLoaderPlugin()
  16. ]
  17. }

最近简单学习了其大致的工作原理,其中也有不少没完全弄懂的地方,但是先简单记录下。

vue-loader 运行过程大致上可以划分为两个阶段:

  1. 预处理阶段:动态修改 Webpack 配置,注入 vue-loader 专用的一系列 module.rules;
  2. 内容处理阶段: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 配置。
插件主要完成两个任务:

  1. 初始化并注册 Pitch Loader:定义pitcher对象,指定loader路径为 require.resolve(‘./loaders/pitcher’) ,并将pitcher注入到 rules 数组首位。这种动态注入的好处是用户不用关注 —— 不去看源码根本不知道还有一个pitcher loader,而且能保证pitcher能在其他rule之前执行,确保运行顺序。
  2. 复制 rules 配置:代码第8行遍历 compiler.options.module.rules 数组,也就是用户提供的 Webpack 配置中的 module.rules 项,对每个rule执行 cloneRule 方法复制规则对象。

之后,将 Webpack 配置修改为 [pitcher, …clonedRules, …rules],cloneRules从开发者提供的配置中复制过来的,内容相似,只是 cloneRule 在复制过程会给这些规则重新定义 resourceQuery

内容处理

image.png

  1. 路径命中 /.vue$/i 规则,调用 vue-loader 生成中间结果 A;
  2. 结果 A 命中 xx.vue?vue 规则,调用 vue-loader Pitch Loader 生成中间结果 B;
  3. 结果 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

  1. 比如对于的代码: index.vue:
  2. ```vue
  3. <template>
  4. <div class="root">hello world</div>
  5. </template>
  6. <script>
  7. export default {
  8. data() {},
  9. mounted() {
  10. console.log("hello world");
  11. },
  12. };
  13. </script>
  14. <style scoped>
  15. .root {
  16. font-size: 12px;
  17. }
  18. </style>
  • 第一次执行 vue-loader ,执行如下逻辑:
  1. 调用 @vue/component-compiler-utils 包的parse函数,将SFC 文本解析为AST对象;
  2. 遍历 AST 对象属性,转换为特殊的引用路径;
  3. 返回转换结果。

第一次转换结果:

  1. import { render, staticRenderFns } from "./index.vue?vue&type=template&id=2964abc9&scoped=true&"
  2. import script from "./index.vue?vue&type=script&lang=js&"
  3. export * from "./index.vue?vue&type=script&lang=js&"
  4. import style0 from "./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"
  5. /* normalize component */
  6. import normalizer from "!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
  7. var component = normalizer(
  8. script,
  9. render,
  10. staticRenderFns,
  11. false,
  12. null,
  13. "2964abc9",
  14. null
  15. )
  16. ...
  17. 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命中到的。

  1. const pitcher = {
  2. loader: require.resolve('./loaders/pitcher'),
  3. resourceQuery: query => {
  4. if (!query) { return false }
  5. const parsed = qs.parse(query.slice(1))
  6. return parsed.vue != null
  7. }
  8. }

命中 xx.vue?vue 格式的路径,也就是说上面 vue-loader 转换后的 import 路径会被 Pitch Loader 命中,做进一步处理.

  • Pitch Loader 的逻辑比较简单,做的事情也只是转换 import 路径。处理后,会得到一个新的行内路径:

    1. 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 函数。

  1. module.exports = function selectBlock (
  2. descriptor,
  3. loaderContext,
  4. query,
  5. appendExtension
  6. ) {
  7. // template
  8. if (query.type === `template`) {
  9. if (appendExtension) {
  10. loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
  11. }
  12. loaderContext.callback(
  13. null,
  14. descriptor.template.content,
  15. descriptor.template.map
  16. )
  17. return
  18. }
  19. // script
  20. if (query.type === `script`) {
  21. if (appendExtension) {
  22. loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
  23. }
  24. loaderContext.callback(
  25. null,
  26. descriptor.script.content,
  27. descriptor.script.map
  28. )
  29. return
  30. }
  31. // styles
  32. if (query.type === `style` && query.index != null) {
  33. const style = descriptor.styles[query.index]
  34. if (appendExtension) {
  35. loaderContext.resourcePath += '.' + (style.lang || 'css')
  36. }
  37. loaderContext.callback(
  38. null,
  39. style.content,
  40. style.map
  41. )
  42. return
  43. }
  44. // custom
  45. if (query.type === 'custom' && query.index != null) {
  46. const block = descriptor.customBlocks[query.index]
  47. loaderContext.callback(
  48. null,
  49. block.content,
  50. block.map
  51. )
  52. return
  53. }
  54. }

至此,就可以完成从 Vue SFC 文件中抽取特定 Block 内容,并复用用户定义的其它 Loader 加载这些 Block。