极客时间-玩转webpack

webpack 启动

image.png
webpack运行命令:

  1. // 通过 npm scripts 运行 webpack
  2. 开发环境: npm run dev
  3. 生产环境:npm run build
  4. // 通过 webpack 直接运行
  5. webpack entry.js bundle.js

在命令行运行以上命令后,npm会让命令行工具进入node_modules\.bin目录查找是否存在webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不存在,就抛出错误。

实际的入口文件是:node_modules\webpack\bin\webpack.js

  1. //1. 正常执行返回
  2. process.exitCode = 0;
  3. //2. 运行某个命令
  4. const runCommand = (command, args) =>{...};
  5. //3. 判断某个包是否安装
  6. const isInstalled = packageName =>{...};
  7. //4. webpack 可用的 CLI: webpack-cli(The original webpack full-featured CLI)和
  8. // webpack-command(A lightweight, opinionated webpack CLI)
  9. const CLIs =[...];
  10. //5. 判断是否两个 ClI 是否安装了
  11. const installedClis = CLIs.filter(cli => cli.installed);
  12. //6. 根据安装数量进行处理
  13. if (installedClis.length === 0){...}else if
  14. (installedClis.length === 1){...}else{...}

启动后的结果:webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且执行 CLI

webpack-cli

webpack-cli 做的事情:

  • 引入yargs,对命令行进行定制
  • 分析命令行参数,对各个参数进行转换,组成编译配置项
  • 引用webpack,根据配置项进行编译和构

不需要编译的命令

webpack-cli 处理不需要经过编译的命令
node_modules\webpack-cli\bin\cli.js

  1. const { NON_COMPILATION_ARGS } = require("./utils/constants");
  2. const NON_COMPILATION_CMD = process.argv.find(arg => {
  3. if (arg === "serve") {
  4. global.process.argv = global.process.argv.filter(a => a !== "serve");
  5. process.argv = global.process.argv;
  6. }
  7. return NON_COMPILATION_ARGS.find(a => a === arg);
  8. });
  9. if (NON_COMPILATION_CMD) {
  10. return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
  11. }

webpack-cli 提供的不需要编译的命令:

  1. const NON_COMPILATION_ARGS = [
  2. "init", // 创建一份 webpack 配置文件
  3. "migrate",// 进行 webpack 版本迁移
  4. "add", // 往webpack 配置文件中增加属性
  5. "remove", // 往 webpack 配置文件中删除属性
  6. "serve", /// 运行 webpack-serve
  7. "generate-loader",// 生成 webpack loader 代码
  8. "generate-plugin", // 生成 webpack plugin 代码
  9. "info"// 返回与本地环境相关的一些信息
  10. ];

命令行工具包 yargs

  • 提供命令和分组参数
  • 动态生成 help 帮助信息

image.pngwebpack-cli 使用 args 分析
参数分组 (config/config-args.js),将命令划分为9类:

  • Config options:配置相关参数(文件名称、运行环境等)
  • Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
  • Module options:模块参数,给 loader 设置扩展
  • Output options::输出参数(输出路径、输出文件名称)
  • Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)
  • Resolving options:解析参数(alias 和 解析的文件后缀设置)
  • Optimizing options:优化参数
  • Stats options:统计参数
  • options:通用参数(帮助命令、版本信息等)

执行的结果

  1. options = require("./utils/convert-argv")(argv);
  1. compiler = webpack(options);

webpack-cli 对配置文件命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化 webpack 对象,然后执行构建流程。

webpack 的本质

webpack 可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

  1. class Compiler extends Tapable {
  2. // ...
  3. }
  4. class Compilation extends Tapable {
  5. // ...
  6. }

Tapable

Tapable(水龙头)是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

Tapable库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

  1. const {
  2. SyncHook, // 同步钩子
  3. SyncBailHook, // 同步熔断钩子
  4. SyncWaterfallHook, // 同步流水钩子
  5. SyncLoopHook, // 同步循环钩子
  6. AsyncParallelHook, // 异步并发钩子
  7. AsyncParallelBailHook, // 异步并发熔断钩子
  8. AsyncSeriesHook, // 异步串行钩子
  9. AsyncSeriesBailHook, / /异步串行熔断钩子
  10. AsyncSeriesWaterfallHook // 异步串行流水钩子
  11. } = require("tapable");

Tapable hooks 类型
image.png

Tapable 的使用 -new Hook 新建钩子

Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子

class 接受数组参数 options ,非必传。类方法会根据传参,接受同样数量的参数。

  1. const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);

Tapable 的使用-钩子的绑定与执行
Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。
image.png

Tapable 的使用-hook 基本用法示例

  1. const {
  2. SyncHook
  3. } = require('tapable');
  4. const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
  5. //绑定事件到 webapck事件流
  6. hook.tap('hook1', (arg1, arg2, arg3) => {
  7. console.log(arg1, arg2, arg3);
  8. });
  9. //执行绑定的事件
  10. hook.call(1, 2, 3); // 1 2 3

Tapable 的使用-实际例子演示

  1. /**
  2. * 1.定义一个 Car 方法,在内部 hooks 上新建钩子。
  3. * 分别是同步钩子 accelerate、brake( accelerate 接受一个参数)、
  4. * 异步钩子 calculateRoutes
  5. *
  6. * 2.使用钩子对应的绑定和执行方法
  7. *
  8. * 3.calculateRoutes 使用 tapPromise 可以返回一个 promise 对象
  9. */
  10. const {
  11. SyncHook,
  12. AsyncSeriesHook
  13. } = require('tapable');
  14. class Car {
  15. constructor() {
  16. this.hooks = {
  17. accelerate: new SyncHook(['newspeed']), //加速
  18. brake: new SyncHook(), //刹车
  19. calculateRoutes:
  20. new AsyncSeriesHook(["source", "target", "routesList"]) //计算路径
  21. }
  22. }
  23. }
  24. const myCar = new Car();
  25. //绑定同步钩子
  26. myCar.hooks.brake.tap("WarningLampPlugin", () =>
  27. console.log('--WarningLampPlugin'));
  28. //绑定同步钩子 并传参
  29. myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed =>
  30. console.log(`--Accelerating to ${newSpeed}`));
  31. //绑定一个异步Promise钩子
  32. myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise",
  33. (source, target, routesList, callback) => {
  34. // return a promise
  35. return new Promise((resolve, reject) => {
  36. setTimeout(() => {
  37. console.log(`--tapPromise to ${source} ${target} ${routesList}`)
  38. resolve();
  39. }, 1000)
  40. })
  41. });
  42. myCar.hooks.brake.call();
  43. myCar.hooks.accelerate.call(10);
  44. console.time('cost');
  45. //执行异步钩子
  46. myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo')
  47. .then(() => {
  48. console.timeEnd('cost');
  49. }, err => {
  50. console.error(err);
  51. console.timeEnd('cost');
  52. });
  53. /**
  54. * 打印:
  55. * --WarningLampPlugin
  56. * --Accelerating to 10
  57. * --tapPromise to Async hook demo
  58. * cost: 1.005s
  59. */

Tapable 是如何和 webpack 联系起来的?
node_modules\webpack\lib\webpack.js

  1. if (Array.isArray(options)) {
  2. compiler = new MultiCompiler(options.map(options => webpack(options)));
  3. } else if (typeof options === "object") {
  4. options = new WebpackOptionsDefaulter().process(options);
  5. compiler = new Compiler(options.context);
  6. compiler.options = options;
  7. new NodeEnvironmentPlugin().apply(compiler);
  8. // plugins数组
  9. if (options.plugins && Array.isArray(options.plugins)) {
  10. for (const plugin of options.plugins) {
  11. if (typeof plugin === "function") {
  12. plugin.call(compiler, compiler);
  13. } else {
  14. plugin.apply(compiler);
  15. }
  16. }
  17. }
  18. compiler.hooks.environment.call();
  19. compiler.hooks.afterEnvironment.call();
  20. // 注入内部插件
  21. compiler.options = new WebpackOptionsApply().process(options, compiler);
  22. }
  • 插件必须有 apply 方法,接受 compiler 参数
  • 插件监听 compiler 上的 hooks,执行对应的动作

模拟 Compiler.js

  1. const {
  2. SyncHook,
  3. AsyncSeriesHook
  4. } = require('tapable');
  5. module.exports = class Compiler {
  6. constructor() {
  7. this.hooks = {
  8. accelerate: new SyncHook(['newspeed']),
  9. brake: new SyncHook(),
  10. calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
  11. }
  12. }
  13. // 入口
  14. run() {
  15. this.accelerate(10)
  16. this.break()
  17. this.calculateRoutes('Async', 'hook', 'demo')
  18. }
  19. accelerate(speed) {
  20. this.hooks.accelerate.call(speed);
  21. }
  22. break() {
  23. this.hooks.brake.call();
  24. }
  25. calculateRoutes() {
  26. this.hooks.calculateRoutes.promise(...arguments).then(() => {
  27. }, err => {
  28. console.error(err);
  29. });
  30. }
  31. }

插件 my-plugin.js

  1. const Compiler = require('./Compiler')
  2. class MyPlugin {
  3. constructor() {
  4. }
  5. // 监听 hook
  6. apply(compiler) {
  7. compiler.hooks.brake.tap("WarningLampPlugin", () =>
  8. console.log('WarningLampPlugin'));
  9. compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed =>
  10. console.log(`Accelerating to ${newSpeed}`));
  11. compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync",
  12. (source, target, routesList) => {
  13. return new Promise((resolve, reject) => {
  14. setTimeout(() => {
  15. console.log(`tapPromise to ${source} ${target} ${routesList}`)
  16. resolve();
  17. }, 1000)
  18. });
  19. });
  20. }
  21. }

模拟插件执行

  1. // 模拟插件执行
  2. const myPlugin = new MyPlugin();
  3. const options = {
  4. plugins: [myPlugin]
  5. }
  6. const compiler = new Compiler();
  7. for (const plugin of options.plugins) {
  8. if (typeof plugin === "function") {
  9. plugin.call(compiler, compiler);
  10. } else {
  11. plugin.apply(compiler); //插件监听compiler的hooks
  12. }
  13. }
  14. compiler.run(); //开始编译,内部会触发一系列的hooks

webpack 流程image.png

WebpackOptionsApply

将所有的配置 options 参数转换成 webpack 内部插件
使用默认插件列表
举例:

  • output.library -> LibraryTemplatePlugin
  • externals -> ExternalsPlugin
  • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
  • AMDPlugin, CommonJsPlugin
  • RemoveEmptyChunksPlug

    阶段

  • 准备阶段

  • 打包构建阶段
  • 优化输出阶段

Compiler hooks

流程相关:

  • (before-)run
  • (before-/after-)compile
  • make
  • (after-)emit
  • done

监听相关:

  • watch-run
  • watch-close

Compilation

Compiler 调用 Compilation 生命周期方法

  • addEntry -> addModuleChain
  • finish (上报模块错误)
  • seal(输出)

ModuleFactory

image.png

Module

image.png

NormalModule

Build

  • 使用 loader-runner 运行 loaders
  • 通过 Parser 解析 (内部是 acron)
  • ParserPlugins 添加依赖

Compilation hooks

模块相关:

  • build-module
  • failed-module
  • succeed-module

资源生成相关:

  • module-asset
  • chunk-asset

优化和 seal相关:

  • (after-)seal
  • optimize
  • optimize-modules(-basic/advanced)
  • after-optimize-modules
  • after-optimize-chunks
  • after-optimize-tree
  • optimize-chunk-modules(-basic/advanced)
  • after-optimize-chunk-modules
  • optimize-module/chunk-order
  • before-module/chunk-ids
  • (after-)optimize-module/chunk-ids
  • before/after-hash

Chunk 生成算法

  1. webpack 先将 entry 中对应的 module 都生成一个新的 chunk
    2. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
    3. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
    4. 重复上面的过程,直至得到所有的 chunks

模块化

增强代码可读性和维护性

  • 传统的网页开发转变成 Web Apps 开发
  • 代码复杂度在逐步增高
  • 部署时希望把代码优化成几个 HTTP 请求
  • 分离的 JS文件/模块,便于后续代码的维护性

常见的几种模块化方式

  • ES module

    1. import * as largeNumber from 'large-number';
    2. // ...
    3. largeNumber.add('999', '1');
  • CJS

    1. const largeNumbers = require('large-number');
    2. // ...
    3. largeNumber.add('999', '1');
  • AMD

    1. require(['large-number'], function (large-number) {
    2. // ...
    3. largeNumber.add('999', '1');
    4. });

    AST 基础知识

    抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
    在线demo: https://esprima.org/demo/parse.html
    image.png

    webpack 的模块机制

  • 打包出来的是一个 IIFE (匿名闭包)

  • modules 是一个数组,每一项是一个模块初始化函数
  • 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序
  • __webpack_require 用来加载模块,返回 module.exportsimage.png

    实现一个简易的 webpack

  • 可以将 ES6 语法转换成 ES5 的语法

    • 通过 babylon 生成AST
    • 通过 babel-core 将AST重新生成源码
  • 可以分析模块之间的依赖关系
    • 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性
  • 生成的 JS 文件可以在浏览器中运行