webpack全方位由浅到深讲解,做到简历上真正所谓的“熟悉”(系列四)

998 阅读6分钟

通过源代码掌握webpack打包原理

webpack启动过程分析

  • 开始: 从 webpack命令行说起

    • 通过 npm scripts运行webpack
      • 开发环境: npm run dev
      • 生产环境: npm run build
    • 通过webpack直接运行
      • webpack entry.js bundle.js

    这个过程发生了什么?

  • 查找webpack入口文件

    • 在命令行运行以上命令后,npm会让命令行工具进入node_modules/.bin目录查找是否存在webpack.sh(mac或linux系统)或webpack.cmd(windows系统)文件, 如果存在,就执行,不存在,就抛出错误。

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

    运行一个命令,如果是全局安装,mac或linux会从useLocal/bin下查找; 如果是局部安装,会从当前目录下的node_modules/.bin目录查找,由于webpack和webpack-cli是局部安装,所以是从node_modules/.bin目录查找

    • 哪个包提供了webpack命令呢? webpack还是webpack-cli呢 - 查看webpack和webpack-cli,发现是在webpack下提供此命令的

image.png

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

webpack-cli源码分析

  • webpack-cli做的事情
    • 引入yargs, 对命令行进行定制
    • 分析命令行参数,对各个参数进行转换, 组成编译配置项
    • 引用webpack, 根据配置项进行编译和构建
  • webpack-cli处理不需要经过编译的命令
    • 从 NON_COMPILATION_CMD分析出不需要编译的命令
    • 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帮助信息
  • 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可以将其理解是一种基于事件流的编程范例,一系列的插件运行

  • 核心对象 Compiler 继承 Tapable

  • 核心对象 Compilation 继承 Tapable

  • Tapable是什么?

    • Tapable是一个类似于Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。
    • Tapable库暴露里很多Hook(钩子)类,为插件提供挂载的钩子
    const {
        SyncHook,                    // 同步钩子
        SyncBailHook,                //同步熔断钩子
        SyncWaterfallHook,           //同步流水钩子
        SyncLoopHook,                //同步循环钩子 
        AsyncParallelHook,           //异步并发钩子
        AsyncParallelBailHook,       //异步并发熔断钩子
        AsyncSeriesHook,             //异步串行钩子
        AsyncSeriesBailHook,         //异步串行熔断钩子
        AsyncSeriesWaterfallHook     //异步串行流水钩子
    } = require("tapable ")
    
    typefunction
    Hook所有钩子的后缀
    Waterfall同步方法,但是它会传值给下一个函数
    Bail熔断:当函数有任何返回值,就会在当前执行函数停止
    Loop监听函数返回true表示继续循环,返回undefine表示循环结束
    Sync同步方法
    AsyncSeries异步串行钩子
    AsyncParallel异步并行执行钩子
  • Tapabel的使用-new Hook 新建钩子

    • Tapabel暴露出来的都是类方法, new 一个类方法获得我们需要的钩子
    • class 接收数组参数options,非必传。 类方法会根据传参, 接收同样数量的参数。
      const hook1 = new SyncHook(["arg1","arg2","arg3"]);
      
  • Tapable的使用-钩子的绑定与执行

    • Tabpack提供了同步&异步绑定钩子 的方法,并且他们都有绑定事件和执行事件对应的方法
    Async*Sync*
    绑定:tapAsync/tapPromise/tap绑定:tap
    执行:callAsync/promise执行:call
  • Tapable 的使用-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)
    
  • Tapable 的使用-实际例子演示

    定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、
    brake( accelerate 接受一个参数)、异步钩子 calculateRoutes
    
    使用钩子对应的绑定和执行方法
    
    calculateRoutes 使用 tapPromise 可以返回一个 promise 对象
    
    const {
        SyncHook,
        AsyncSeriesHook
    } = require('tapable');
    
    class Car {
        constructor() {
            this.hooks = {
                accelerate: new SyncHook(['newspeed']),
                brake: new SyncHook(),
                calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
            }
        }
    }
    
    const myCar = new Car();
    
    //绑定同步钩子
    myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
    
    //绑定同步钩子 并传参
    myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
    
    //绑定一个异步Promise钩子
    myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
        // return a promise
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log(`tapPromise to ${source} ${target} ${routesList}`)
                resolve();
            },1000)
        })
    });
    
    myCar.hooks.brake.call();
    myCar.hooks.accelerate.call(10);
    
    console.time('cost');
    
    //执行异步钩子
    myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo').then(() => {
        console.timeEnd('cost');
    }, err => {
        console.error(err);
        console.timeEnd('cost');
    });
    

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.options = options;
   new NodeEnvironmentPlugin().apply(compiler);
   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
    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
    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)
                });
            });
        }
    }
    
  • 模拟插件执行
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();

Webpack 流程篇

  • webpack的编译都按照下面的钩子调用顺序执行

image.png

  • WebpackOptionsApply
    • 将所有的配置 options 参数转换成 webpack 内部插件
    • 使用默认插件列表
    • 举例:
      • output.library -> LibraryTemplatePlugin
      • externals -> ExternalsPlugin
      • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
      • AMDPlugin, CommonJsPlugin
      • RemoveEmptyChunksPlugin
  • 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
        1. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
        1. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
        1. 重复上面的过程,直至得到所有的 chunks

最后:

如果你有梦想,一定要来大城市,这里可以帮您实现你想要的

有些梦想,需要借助城市的力量才能实现

其它系列文章链接