Introduce to Webpack

date
Jun 8, 2021
slug
webpack
status
Published
tags
webpack
summary
webpack简介
type
Post
 

工作流程

  1. 读取配置参数config.js
  1. 启动webpack,创建Compiler对象并开始解析项目
  1. 以entry文件为入口跑dfs,生成依赖关系树
  1. 对树上的节点根据不同的文件类型,使用不同的loader进行处理,最终输出产物为JavaScript文件,loader可以理解成纯函数。
  1. 在1-4的流程中,webpack会基于发布订阅模式,抛出一些hook,wepack的插件系统就是基于这些hook,执行插件的功能,plugin可以理解成执行副作用
 

源码细节

webpack中有两个核心对象,compiler和compilation,compiler是一个singleton,负责控制webpack的构建流程;compliation是每一次构建时的上下文对象,包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个compilation对象,来维护这次的构建过程。
在loader处理每个节点的同时,会用acorn库生成节点的AST,将该节点AST合并到总的AST的中,如果当前AST还有依赖,则dfs进入下一层节点。
最终webpack打包出来的bundle文件是一个llfe函数
// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
    var __webpack_modules__ = ({
        'file-A-path': ((modules) => { // ... })
        'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
    })
    
    // The module cache
    var __webpack_module_cache__ = {};
    
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
                return cachedModule.exports;
        }
        // Create a new module (and put it into the cache)
        var module = __webpack_module_cache__[moduleId] = {
                // no module.id needed
                // no module.loaded needed
                exports: {}
        };

        // Execute the module function
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

        // Return the exports of the module
        return module.exports;
    }
    
    // startup
    // Load entry module and return exports
    // This entry module can't be inlined because the eval devtool is used.
    var __webpack_exports__ = __webpack_require__("./src/index.js");
})
整个llfe只有三个变量和一个方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。
__webpack_require__模块引入函数存在的原因是,存在有ES ModuleCommonJS两种规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性
 

Loader

Webpack的最终输出是一份Javascript代码,在Webpack内部也只能够处理Javascript代码,因此当遇到非Javascript文件时,需要将其转换成等效的Javascript文件,才能继续执行打包任务。Loader的作用就是转换非JavaScript文件。
Loader的配置如下
// webpack.config.js
module.exports = {
  // ...other config
  module: {
    rules: [
      {
        test: /^your-regExp$/,
        use: [
          {
             loader: 'loader-name-A',
          }, 
          {
             loader: 'loader-name-B',
          }
        ]
      },
    ]
  }
}
对于某个匹配规则,会有一个Loader数组来链式处理,上一个loader的输出会作为下一个loader的输入。因此loader必须是JavaScript代码字符串。
loader并不是一个纯函数,它的this上下文由webpack提供,this指向loaderContext的loaderRunnder对象
module.exports = function(source) {
    const content = doSomeThing2JsString(source);
    
    // 如果 loader 配置了 options 对象,那么this.query将指向 options
    const options = this.query;
    
    // 可以用作解析其他模块路径的上下文
    console.log('this.context', this.context);
    
    /*
     * this.callback 参数:
     * error:Error | null,当 loader 出错时向外抛出一个 error
     * content:String | Buffer,经过 loader 编译后需要导出的内容
     * sourceMap:为方便调试生成的编译后内容的 source map
     * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
     */
    this.callback(null, content);
    // or return content;
}
 

Plugin

plugin可以扩展webpack的功能
之前提到过webpack的两个核心对象compiler和compilation,其中compiler上暴露了Webpack整个生命周期相关的钩子(compiler hook),compilation则暴露出模块和依赖相关的粒度更小的事件钩子
Webpack的事件机制基于webpack自己实现的一套 tapable事件流方案
// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
    constructor() {
        // 在this.hooks中定义所有的钩子事件
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            brake: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
    }

    /* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
 
plugin也有一些开发的规范:
  • plugin必须是一个函数或者拥有apply方法的对象,这样才能传入compiler实例
  • 传入每个plugin的compiler是同一个实例,如果在某个插件中对compiler有修改,会影响到后续的plugin的complier
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
class MyPlugin {
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 当前打包构建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}
 
SourceMap
 
sourceMap可以将被压缩混淆的代码还原出来,通过一份映射记录的map文件可以将混淆前后的代码一一对应。sourceMap文件的数据结构如下
{
  "version" : 3,                          // Source Map版本
  "file": "out.js",                       // 输出文件(可选)
  "sourceRoot": "",                       // 源文件根目录(可选)
  "sources": ["foo.js", "bar.js"],        // 源文件列表
  "sourcesContent": [null, null],         // 源内容列表(可选,和源文件列表顺序一致)
  "names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
  "mappings": "A,AAAB;;ABCDE;"            // 带有编码映射数据的字符串
}
map文件需要以下规则
  • 生成文件中的一行的每个组用“;”分隔;
  • 每一段用“,”分隔;
  • 每个段由1、4或5个可变长度字段组成;
有了sourceMap文件后,可以在混淆后的代码后面加一行
//# sourceURL=/path/to/file.js.map
有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射,sourceMap也需要浏览器支持才行
webpack打包出来的bundle文件,可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。
 

© Itisssennsinn 2020 - 2025