整体架构

compiler对象
compilation对象

compiler 算是核心实例,webpack的整个控制器。挂载webpack的整个配置,webpack编译等流程的执行,插件事件的触发和监听。

compilation 是一次构建

编译关键部分

Compiler

继承自Tapable

  1. class Compiler extends Tapable {
  2. constructor(context) {
  3. super();
  4. this.hooks = {
  5. shouldEmit: new SyncBailHook(["compilation"]),
  6. done: new AsyncSeriesHook(["stats"]),
  7. additionalPass: new AsyncSeriesHook([]),
  8. beforeRun: new AsyncSeriesHook(["compilation"]),
  9. run: new AsyncSeriesHook(["compilation"]),
  10. emit: new AsyncSeriesHook(["compilation"]),
  11. afterEmit: new AsyncSeriesHook(["compilation"]),
  12. thisCompilation: new SyncHook(["compilation", "params"]),
  13. compilation: new SyncHook(["compilation", "params"]),
  14. ...
  15. }
  16. }
  17. compile(callback) {
  18. const params = this.newCompilationParams();
  19. this.hooks.beforeCompile.callAsync(params, err => {
  20. if (err) return callback(err);
  21. this.hooks.compile.call(params);
  22. // 生成一个新的compilation
  23. const compilation = this.newCompilation(params);
  24. // make事件
  25. this.hooks.make.callAsync(compilation, err => {
  26. if (err) return callback(err);
  27. compilation.finish();
  28. compilation.seal(err => {
  29. if (err) return callback(err);
  30. // 触发afterCompile事件
  31. this.hooks.afterCompile.callAsync(compilation, err => {
  32. if (err) return callback(err);
  33. return callback(null, compilation);
  34. });
  35. });
  36. });
  37. });
  38. }
  39. }
  40. 属性
  41. hooks // Tabable对外的钩子,所有事件都可以在这里统一看到
  42. resolvers //
  43. records
  44. context // 类似process.cwd()
  45. options
  46. 方法
  47. constructor
  48. watch
  49. run
  50. createChildCompiler
  51. createCompilation
  52. newCompilation
  53. createNormalModuleFactory
  54. createContextModuleFactory
  55. compile

关键事件 & 编译过程

浅谈Webpack工作流程 | YoungZhang’s Blog 参见本篇文章,写的比较详细的编译过程和相应触发的事件

compile -> make -> build module -> after compile -> emit -> after emit

  1. this.hooks.emit.callAsync(compilation, err => {
  2. if (err) return callback(err);
  3. outputPath = compilation.getPath(this.outputPath);
  4. this.outputFileSystem.mkdirp(outputPath, emitFiles);
  5. });

Compilation

  1. class Compilation extends Tapable {
  2. constructor(compiler) {
  3. }
  4. }
  5. 属性
  6. hooks //
  7. compiler // 初始化时传入外部的compiler
  8. entries
  9. chunks
  10. modules
  11. cache
  12. records
  13. assets
  14. errors
  15. warnings
  16. children
  17. 方法
  18. getStats
  19. addModule
  20. findModule
  21. buildModule
  22. addModuleDependencies
  23. addEntry
  24. prefetch
  25. rebuildModule
  26. finish
  27. unseal
  28. seal

关键事件

Tapable

webpack/tapable: Just a little module for plugins.
关于 tapable 你需要知道这些 - 知乎 非常简单的介绍清楚了Tapable的用户

webpack中特征,在某个事件会注册诸多的插件,插件机制在不同事件有不同的处理机制,因此有了类似EventEmitter,又比起增多了更多编排处理。

  • SyncHook 类似EventEmitter
  • SyncBailHook 注册的回调返回非undefiend时,就停止不再执行其他回调
  • SyncWaterfallHook 接受至少一个参数,上一个注册的回调返回值会作为下一个注册的回调的参数。

  • AsyncParallelHook 异步并发钩子,所有注册回调并发执行,当所有注册的异步回调并行执行完成后,再执行callAsync中的函数

  • AsyncSeriesHook 顺序执行钩子,
  • AsyncParallelBailHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook
  1. const {
  2. Tapable,
  3. SyncHook, //
  4. SyncBailHook,
  5. AsyncParallelHook,
  6. AsyncSeriesHook
  7. } = require("tapable");
  8. // 继承自Tabple
  9. class Compiler extends Tapable {
  10. constructor(context) {
  11. super();
  12. // hooks的初始化,定义是什么类型的hooks,hooks的参数
  13. this.hooks = {
  14. shouldEmit: new SyncBailHook(["compilation"]),
  15. done: new AsyncSeriesHook(["stats"]),
  16. additionalPass: new AsyncSeriesHook([]),
  17. beforeRun: new AsyncSeriesHook(["compilation"]),
  18. run: new AsyncSeriesHook(["compilation"]),
  19. emit: new AsyncSeriesHook(["compilation"]),
  20. compilation: new SyncHook(["compilation", "params"]),
  21. }
  22. }
  23. }
  24. // 同步的hooks调用
  25. if (this.hooks.shouldEmit.call(compilation) === false) {
  26. }
  27. // 异步的hooks调用
  28. this.hooks.done.callAsync(stats, err => {
  29. });

插件机制

如何编写插件?

  • 编写一个类,constructor中处理初始化参数,
  • apply的模板方法注入compiler对象,
  • compiler对象可供做监听事件钩子
  • 插件钩子提供一个回调函数,拿到编译时的compilation对象,可以做各种获取和修改(编译信息,文件内容,等等)

摘自 探寻 webpack 插件机制 的一段代码

  1. class AnalyzeWebpackPlugin {
  2. constructor(opts = { filename: 'analyze.html' }) {
  3. this.opts = opts
  4. }
  5. apply(compiler) {
  6. const self = this
  7. compiler.plugin("emit", function (compilation, callback) {
  8. let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态
  9. let stringifiedStats = JSON.stringify(stats)
  10. // 服务端渲染
  11. let html = `<!doctype html>
  12. <meta charset="UTF-8">
  13. <title>AnalyzeWebpackPlugin</title>
  14. <style>${cssString}</style>
  15. <div id="App"></div>
  16. <script>window.stats = ${stringifiedStats};</script>
  17. <script>${jsString}</script>
  18. `
  19. compilation.assets[`${self.opts.filename}`] = { // 生成文件路径
  20. source: () => html,
  21. size: () => html.length
  22. }
  23. callback()
  24. })
  25. }
  26. }

探寻 webpack 插件机制
WebPack 插件机制探索 – 滴滴云博客
webpack插件机制剖析 | 大专栏

Loader

阅读 vue-loader

vuejs/component-compiler-utils: Lower level utilities for compiling Vue single file components

vue-loader/lib/index.js

  1. const loaderUtils = require('loader-utils') // 非常重要,官方提供的
  2. const { parse } = require('@vue/component-compiler-utils')
  3. //
  4. module.exports = function (source) {
  5. const loaderContext = this;
  6. const {
  7. target,
  8. request,
  9. minimize,
  10. sourceMap,
  11. rootContext,
  12. resourcePath,
  13. resourceQuery = ''
  14. } = loaderContext
  15. const options = loaderUtils.getOptions(loaderContext) || {} // options.compiler
  16. const descriptor = parse({
  17. source,
  18. compiler: options.compiler || loadTemplateCompiler(loaderContext),
  19. filename,
  20. sourceRoot,
  21. needMap: sourceMap
  22. })
  23. descriptor.styles // <style></style> 可以是多个标签
  24. descriptor.template // <template></template>标签包裹的内容
  25. descriptor.script // <script></script> 标签
  26. }
  27. // 大概转义的结果是
  28. // template
  29. let templateImport;
  30. const src = descriptor.template.src || resourcePath;
  31. const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`;
  32. const request = templateRequest = stringifyRequest(src + query);
  33. templateImport = `import { render, staticRenderFns } from ${request}`;
  34. // script
  35. const request = stringifyRequest(src + query)
  36. scriptImport = (
  37. `import script from ${request}\n` +
  38. `export * from ${request}` // support named exports
  39. )
  40. let code = `
  41. ${templateImport}
  42. ${scriptImport}
  43. ${stylesCode}
  44. /* normalize component */
  45. import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
  46. var component = normalizer(
  47. script,
  48. render,
  49. staticRenderFns,
  50. ${hasFunctional ? `true` : `false`},
  51. ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
  52. ${hasScoped ? JSON.stringify(id) : `null`},
  53. ${isServer ? JSON.stringify(hash(request)) : `null`}
  54. ${isShadow ? `,true` : ``}
  55. )
  56. `.trim() + `\n`

Plugin的作用?

实践 经常loader和plugin配置

很多的插件是如何loader和plugin配合完成任务,如vue-loader中包含vue-loader和VueLoaderPlugin

MiniCssExtractPlugin | webpack

  1. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  2. module.exports = {
  3. plugins: [new MiniCssExtractPlugin()],
  4. module: {
  5. rules: [
  6. {
  7. test: /\.css$/i,
  8. use: [MiniCssExtractPlugin.loader, 'css-loader'],
  9. },
  10. ],
  11. },
  12. };

vue-loader

实践

编译优化

主要是本地编译速度的优化

webpack 编译优化 · Issue #5 · jpone223/blog

性能优化

为最终线上运行production的优化

  • long-term-caching
  • chunks 拆包 (可按被引用的次数)
  • webpack dll
  • tree shaking

查漏补缺 面试题

「吐血整理」再来一打Webpack面试题

一些想法

  • 不要惧怕看源码,可能并不复杂,搞清楚核心模块,以及核心模块的关系就很好阅读

如webpack先看文章,

  • webpack的核心部分还是非常非常清晰的,compiler,compilation