webpack启动过程分析

webpack命令行

通过 npm scripts 运行 webpack

  • 开发环境: npm run dev
  • 生产环境:npm run build

    通过 webpack 直接运行

  • webpack entry.js bundle.js

    查找 webpack 入口文件

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

    分析 webpack 的入口文件:webpack.js

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

    启动后的结果

    webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且执行 CLI

    webpack-cli源码阅读

    webpack-cli 做的事情

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

    从NON_COMPILATION_CMD分析出不需要编译的命令

    webpack-cli 处理不需要经过编译的命令

    const { NON_COMPILATION_ARGS } = require("./utils/constants");
    const NON_COMPILATION_CMD = process.argv.find(arg => {
    if (arg === "serve") {
      global.process.argv = global.process.argv.filter(a => a !== "serve");
      process.argv = global.process.argv;
    }
    return NON_COMPILATION_ARGS.find(a => a === arg);
    });
    if (NON_COMPILATION_CMD) {
      return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
    }
    

    NON_COMPILATION_ARGS的内容

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

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

    命令行工具包 yargs 介绍

    提供命令和分组参数
    动态生成 help 帮助信息截屏2022-02-21 下午4.47.17.png

    webpack-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: 通用参数(帮助命令、版本信息等)

    webpack-cli 执行的结果

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

    Tapable插件架构与Hooks设计

    Webpack 的本质

    Webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行。截屏2022-02-21 下午4.51.51.png
    Tapable 是什么?
    Tapable 是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。
    Tapable库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子
    const {
    SyncHook, //同步钩子
    SyncBailHook, //同步熔断钩子
    SyncWaterfallHook, //同步流水钩子
    SyncLoopHook, //同步循环钩子
    AsyncParallelHook, //异步并发钩子
    AsyncParallelBailHook, //异步并发熔断钩子
    AsyncSeriesHook, //异步串行钩子
    AsyncSeriesBailHook, //异步串行熔断钩子
    AsyncSeriesWaterfallHook //异步串行流水钩子
    } = require("tapable");
    

    Tapable hooks 类型

    截屏2022-02-21 下午4.59.31.png

    Tapable 的使用

    new Hook 新建钩子

    Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子
    class 接受数组参数 options ,非必传。类方法会根据传参,接受同样数量的参数。
    const hook1 = new SyncHook([“arg1”, “arg2”, “arg3”]);

    钩子的绑定与执行

    Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。截屏2022-02-21 下午5.26.13.png

    hook 基本用法示例

    const hook1 = new SyncHook([“arg1”, “arg2”, “arg3”]); //绑定事件到webapck事件流
    hook1.tap(‘hook1’, (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
    hook1.call(1,2,3)//执行绑定的事件

    实际例子演示

    定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、brake( accelerate 接受一个参数)、异步钩子 calculateRoutes
    使用钩子对应的绑定和执行方法
    calculateRoutes 使用 tapPromise 可以返回一个 promise 对象

    Tapable与webpack关联关系

    if (Array.isArray(options)) {
      compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
      options = new WebpackOptionsDefaulter().process(options);//初始化
      compiler = new Compiler(options.context);//创建compiler对象
      compiler.options = options;
      new NodeEnvironmentPlugin().apply(compiler);//调用NodeEnvironmentPlugin
      if (options.plugins && Array.isArray(options.plugins)) {
      //判断插件,然后遍历,执行
          for (const plugin of options.plugins) {
              if (typeof plugin === "function") {
                  plugin.call(compiler, compiler);
              } else {
                  plugin.apply(compiler);
              }
          }
      }
      compiler.hooks.environment.call();
      compiler.hooks.afterEnvironment.call();
      compiler.options = new WebpackOptionsApply().process(options, compiler);//注入内部插件
    }
    

    模拟 Compiler.js

    //2个同步hook,1个异步hook,1个run方法
    module.exports = class Compiler {
      constructor() {
      this.hooks = {
        accelerate: new SyncHook(['newspeed']), 
        brake: new SyncHook(),
        calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
      }
      }
      run(){
          this.accelerate(10)
          this.break()
          this.calculateRoutes('Async', 'hook', 'demo')
      }
    accelerate(speed) {
        this.hooks.accelerate.call(speed);
    }
    break() {
        this.hooks.brake.call();
    }
    calculateRoutes() {
        this.hooks.calculateRoutes.promise(...arguments).then(() => {
      }, err => {
              console.error(err);
          });
      }
    }
    
    插件 my-plugin.js
    const Compiler = require('./Compiler')
    class MyPlugin{
      constructor() {
      }
      apply(compiler){
          compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
          compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
          compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) => {
              return new Promise((resolve,reject)=>{
                  setTimeout(()=>{
                      console.log(`tapPromise to ${source} ${target} ${routesList}`)
                      resolve();
                  },1000)
              });
          });
      }
    }
    
    模拟插件执行 ```javascript const myPlugin = new MyPlugin();

const options = { plugins: [myPlugin] } const compiler = new Compiler();

for (const plugin of options.plugins) { if (typeof plugin === “function”) { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } compiler.run();

<a name="kD4Gn"></a>
# webpack流程
webpack的编译都按照下面的钩子调用顺序执行![截屏2022-02-21 下午8.52.18.png](https://cdn.nlark.com/yuque/0/2022/png/8394412/1645447942966-4a51dace-f42c-4fb1-bedf-ed617b1cfc7d.png#clientId=u64572f86-fc26-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=uc63aaff2&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-21%20%E4%B8%8B%E5%8D%888.52.18.png&originHeight=548&originWidth=1848&originalType=binary&ratio=1&rotation=0&showTitle=false&size=194352&status=done&style=none&taskId=ub468e0d7-a92b-48da-8879-e845781c557&title=)
<a name="ZaJYB"></a>
## 准备阶段
<a name="eYRmu"></a>
### WebpackOptionsApply
将所有的配置 options 参数转换成 webpack 内部插件<br />使用默认插件列表<br />举例:

- output.library -> LibraryTemplatePlugin
- externals -> ExternalsPlugin
- devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
- AMDPlugin, CommonJsPlugin
- RemoveEmptyChunksPlugin
<a name="p6E6u"></a>
## 模块构建和chunk生成阶段
<a name="iRWUr"></a>
### Compiler hooks

- 流程相关:
- (before-)run
- (before-/after-)compile
- make
- (after-)emit ·done

监听相关:

- watch-run
- watch-close

资源生成相关:

- 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
<a name="DZwfF"></a>
### Compilation
Compiler 调用 Compilation 生命周期方法

- addEntry -> addModuleChain
- finish (上报模块错误) 
- seal
<a name="rEUFZ"></a>
### ModuleFactory
![截屏2022-02-22 下午7.26.59.png](https://cdn.nlark.com/yuque/0/2022/png/8394412/1645529223291-8b44b27a-6b2e-41d9-9489-466571ecebec.png#clientId=u64572f86-fc26-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u202faab8&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-22%20%E4%B8%8B%E5%8D%887.26.59.png&originHeight=314&originWidth=976&originalType=binary&ratio=1&rotation=0&showTitle=false&size=40017&status=done&style=none&taskId=u262279a9-1978-4a0e-b8ed-c4360bd0be8&title=)
<a name="PBES3"></a>
#### Module
![截屏2022-02-22 下午7.29.04.png](https://cdn.nlark.com/yuque/0/2022/png/8394412/1645529348302-1eb02fbe-d6e3-42cc-b1c9-a9850b944670.png#clientId=u64572f86-fc26-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u91af75a3&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-22%20%E4%B8%8B%E5%8D%887.29.04.png&originHeight=548&originWidth=1730&originalType=binary&ratio=1&rotation=0&showTitle=false&size=124349&status=done&style=none&taskId=ud9d2a3b3-4d28-4ac1-be27-6ced225fc49&title=)
<a name="UgWeT"></a>
#### NormalModule
Build

- 使用 loader-runner 运行 loaders
- 通过 Parser 解析 (内部是 acron) 
- ParserPlugins 添加依赖
<a name="K4CR8"></a>
### Chunk 生成算法
1. webpack 先将 entry 中对应的 module 都生成一个新的 chunk<br />2. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中<br />3. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖<br />4. 重复上面的过程,直至得到所有的 chunks
<a name="it4XV"></a>
## 文件生成阶段

<a name="Jz5tE"></a>
# 动手编写简易webpack
<a name="CBeNd"></a>
## 模块化:增强代码可读性和维护性
传统的网页开发转变成 Web Apps 开发<br />代码复杂度在逐步增高<br />分离的 JS文件/模块,便于后续代码的维护性<br />部署时希望把代码优化成几个 HTTP 请求
<a name="RCjJT"></a>
## 常见的几种模块化方式
ES module
```javascript
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1');

CJS

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

AMD

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

AST 基础知识

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

截屏2022-02-24 下午4.01.13.png

简易的 webpack要求

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

  • 通过 babylon 生成AST
  • 通过 babel-core 将AST重新生成源码

可以分析模块之间的依赖关系

  • 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性

生成的 JS 文件可以在浏览器中运行