原文参考: https://zhuanlan.zhihu.com/p/341692197

一:整体

1、单文件组件(SFC,.vue 文件)

以 Webpack4 为例

  1. <template>
  2. </template>
  3. <script>
  4. export default {
  5. }
  6. </script>
  7. <style>
  8. </style>

Webpack 需要增加 vue-loader 和 vueLoaderPlugin 对 SFC 进行支持。
样式: vue-style-loader、 css-loader

2、template、script、style 代码块切分

打包编译之后会被转化为 import 引入
./packages/button/src/button.vue?vue&type=template&id=ca859fb4
./packages/button/src/button-group.vue?vue&type=script&lang=js
image.png
例如: foo.vue?type=template&id=xxxxx

  1. module.exports = function (source) {
  2. // ...
  3. // 解析源码,得到描述符
  4. const descriptor = parse({ source, ... });
  5. // 处理 template 块
  6. let templateImport = `var render, staticRenderFns`
  7. let templateRequest
  8. if (descriptor.template) {
  9. const src = descriptor.template.src || resourcePath
  10. const idQuery = `&id=${id}`
  11. const scopedQuery = hasScoped ? `&scoped=true` : ``
  12. const attrsQuery = attrsToQuery(descriptor.template.attrs)
  13. const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
  14. const request = templateRequest = stringifyRequest(src + query)
  15. templateImport = `import { render, staticRenderFns } from ${request}`
  16. }
  17. // 处理 script 块
  18. let scriptImport = `var script = {}`
  19. if (descriptor.script) {
  20. const src = descriptor.script.src || resourcePath
  21. const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
  22. const query = `?vue&type=script${attrsQuery}${inheritQuery}`
  23. const request = stringifyRequest(src + query)
  24. scriptImport = (
  25. `import script from ${request}\n` +
  26. `export * from ${request}` // support named exports
  27. )
  28. }
  29. // 处理 styles 块 (支持多 style 块)
  30. let stylesCode = ``
  31. if (descriptor.styles.length) {
  32. stylesCode = genStylesCode(
  33. loaderContext,
  34. descriptor.styles,
  35. id,
  36. resourcePath,
  37. stringifyRequest,
  38. needsHotReload,
  39. isServer || isShadow // needs explicit injection?
  40. )
  41. }
  42. // Vue 还支持自定义块
  43. if (descriptor.customBlocks && descriptor.customBlocks.length) { ... }
  44. // ...
  45. }

vue-loader把vue文件中的template、javascript、style代码块分别转化为相应的 import 逻辑

二:VueLoaderPlugin 的作用

?vue&type=template 的作用是什么?可以从 VueLoaderPlugin 中找出答案,首先我们先了解 Webpack 中的 Plugin 能做什么。

1. Plugin 的特性

Plugin 的作用,主要有以下两条:

  1. 能够 hook 到在每个编译(compilation)阶段触发的所有关键事件。
  2. 在插件实例的 apply 方法中,可以通过compiler.options获取 Webpack 配置,并进行修改。

VueLoaderPlugin 通过第二个特性,在初始化阶段,对 module.rules 进行动态修改。

2. VueLoaderPlugin 预处理

VueLoaderPlugin 的处理流程中,修改了module.rules,在原来的基础上加入了 pitcher 和 cloneRules这一步的作用是:新增的 rule ,能识别形如?vue&type=template的querystring,让不同语言的代码块匹配到对应的 rule

  1. class VueLoaderPlugin {
  2. apply (compiler) {
  3. // 标识 VueLoaderPlugin 已被加载
  4. // add NS marker so that the loader can detect and report missing plugin
  5. if (compiler.hooks) {
  6. // webpack 4
  7. compiler.hooks.compilation.tap(id, compilation => {
  8. const normalModuleLoader = compilation.hooks.normalModuleLoader
  9. normalModuleLoader.tap(id, loaderContext => {
  10. loaderContext[NS] = true
  11. })
  12. })
  13. } else {
  14. // webpack < 4
  15. compiler.plugin('compilation', compilation => {
  16. compilation.plugin('normal-module-loader', loaderContext => {
  17. loaderContext[NS] = true
  18. })
  19. })
  20. }
  21. // 重头戏,对 Webpack 配置进行修改
  22. const rawRules = compiler.options.module.rules;
  23. const { rules } = new RuleSet(rawRules);
  24. // for each user rule (expect the vue rule), create a cloned rule
  25. // that targets the corresponding language blocks in *.vue files.
  26. const clonedRules = rules
  27. .filter(r => r !== vueRule)
  28. .map(cloneRule)
  29. // global pitcher (responsible for injecting template compiler loader & CSS
  30. // post loader)
  31. const pitcher = {
  32. loader: require.resolve('./loaders/pitcher'),
  33. resourceQuery: query => {
  34. const parsed = qs.parse(query.slice(1))
  35. return parsed.vue != null
  36. },
  37. options: {
  38. cacheDirectory: vueLoaderUse.options.cacheDirectory,
  39. cacheIdentifier: vueLoaderUse.options.cacheIdentifier
  40. }
  41. }
  42. // 替换初始 module.rules,在原有 rule 上,增加 pitcher、clonedRules
  43. compiler.options.module.rules = [
  44. pitcher,
  45. ...clonedRules,
  46. ...rules
  47. ];
  48. }
  49. }

VueLoaderPlugin 对 rules 的修改,用下图可以更直观地理解各部分 Rule 的作用。(这里有一个小的知识点,除了常见的Rule.test选项外,Rule.resourceQuery选项可以对资源的 querystring 进行匹配)
下图展示VueLoaderPlugin 对 module.rules 的修改:
image.png

三:回到 Loader

上一节梳理了 VueLoaderPlugin 在初始化阶段的预处理,这一节我们继续回到构建阶段中,看看以 VueLoader 为中心如何协调其它 Loader ,得到每个代码块的构建结果。同样地,我们先了解一下 Webpack 的 loader 特性。

1、Webpack 的 loader 运行顺序

对于 loader ,我们知道它们的执行是有顺序的,如果是这样的配置,运行的顺序将是 c-loader -> b-loader -> a-loader。

  1. module.exports = {
  2. module: {
  3. rules: [{
  4. ...
  5. use: ['a-loader', 'b-loader', 'c-loader'],
  6. }],
  7. },
  8. };

不过,在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。

  1. |- a-loader `pitch`
  2. |- b-loader `pitch`
  3. |- c-loader `pitch`
  4. |- requested module is picked up as a dependency
  5. |- c-loader normal execution
  6. |- b-loader normal execution
  7. |- a-loader normal execution

并且在 loader 的 pitch 方法中,如果有实际的返回值,将会跳过后续的 loader,比如在 b-loader 的 pitch 中,如果返回了实际值,将会产生下面的执行顺序。

  1. # 注意 a-loader 依然会正常执行,跳过的是 c-loader
  2. |- a-loader `pitch`
  3. |- b-loader `pitch` returns a module
  4. |- a-loader normal execution

知道这个特性,有利于我们理解 SFC 中各代码块在 loader 中的处理顺序。

2、SFC 转化流程

还记得第一节生成的编译结果吗?每个代码块都导出了对应逻辑,我们以 script 块为例,结合第二节的 PitcherLoader 再次进行转化,转化后的结果为:
下图展示script 块的转化流程:
image.png
最后的 import 语句,使用了内联方式的 import 语法,我们拆分一下便于理解

  1. # import
  2. -!../../node_modules/babel-loader/lib/index.js??ref--2-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./demo.vue?vue&type=script&lang=js&";
  3. # Part1 -!
  4. # 将禁用所有已配置的 preLoader loader,但是不禁用 postLoaders
  5. # Part2 ../../node_modules/babel-loader/lib/index.js??ref--2-0
  6. # 参考小节:VueLoaderPlugin 的预处理,demo.vue 会自动添加 .js 后缀,以匹配所有 js Rule,这里使用 babel-loader 处理 js 模块
  7. # Part3 ../../node_modules/vue-loader/lib/index.js??vue-loader-options
  8. # /\.vue$/ 规则匹配到 demo.vue,并使用 vue-loader 处理 .vue 后缀
  9. # Part4 ./demo.vue?vue&type=script&lang=js&

3、PitchLoader

上述的转化发生在 PitchLoader 中,对 PitchLoader 的实现逻辑感兴趣的同学,可以阅读 loader/pitcher.js 的源码:

  1. // PitcherLoader.pitch 方法,所有带 ?vue 的模块请求,都会走到这里
  2. module.exports.pitch = function (remainingRequest) {
  3. // 如 ./demo?vue&type=script&lang=js
  4. // 此时,loaders 是所有能处理 .vue 和 .xxx 的 loader 列表
  5. let loaders = this.loaders;
  6. ...
  7. // 得到 -!babel-loader!vue-loader!
  8. const genRequest = loaders => { ... };
  9. // 处理 style 块 和 template 块,支持
  10. if (query.type === 'style') { ... }
  11. if (query.type === 'template') { ... }
  12. // 处理 script 块和 custom 块
  13. return `import mod from ${request}; export default mod; export * from ${request}`;
  14. }

4、再次执行 VueLoader

细心的同学可能发现了,在 PitchLoader 的转化结果中,还是会以 vue-loader 作为第一个处理的 loader,但 vue-loader 不是一开始就转化过了吗 ?与第一次不同的是,这次 vue-loader 的作用,仅仅是把 SFC 中语法块的源码提取出来,并交给后面的 loader 进行处理
下图展示第二次进入 vue-loader
image.png

  1. const selectBlock = require('./select')
  2. // if the query has a type field, this is a language block request
  3. // e.g. foo.vue?type=template&id=xxxxx
  4. // and we will return early
  5. // 如果querystring 包含了 type 参数,则直接返回该块的代码
  6. if (incomingQuery.type) {
  7. return selectBlock(
  8. descriptor,
  9. loaderContext,
  10. incomingQuery,
  11. !!options.appendExtension
  12. )
  13. }
  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-loader 里面的处理逻辑基本已经梳理完成~各部分代码块也传入后续的 loader 中进行解析和转化。

四、总结

image.png