Webpack打包流程笔记

230 阅读7分钟

什么是Webpack

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 深入浅出 webpack 吴浩麟

webpack核心概念

entry:入口

入口可以是单入口或多入口

output:输出

loader:模块转换器

loader可以把不同的资源进行处理,比如ES6转为浏览器可识别的ES5,SassLess可转为CSS

module:{
    rules:[
        {
            test:/\.css$/,
            use:[
                'style-loader',
                'css-loader',
                'less-loader'
            ]
        }
    ]
}
//另外一种loader写成对象模式
module:{
    rules:[
        {
            test:/\.css$/,
            use:[
                {
                    loader:'style-loader',
                    options:{
                        insertAt:'top'
                    }
                },
                'css-loader',
                'less-loader'
            ]
        }
    ]
}

loader顺序必须是从less转为css再通过style标签插入到html文件中。

loader读取配置内容的顺序从下到上、从右到左,可以联想成堆,先进后出。

plugins:扩展插件

plugins传入的是个数组,不像loader插件是没有先后顺序的。

mode: 打包模式

分为两种:development和production 实际开发中可能需要更加详细的区分webpack配置,可以配置不同的webpack文件如webpack.base.conf.js、webpack.dev.conf.js、webpack.prod.conf.js,配合script脚本使用。

devtool

optimization优化项

modeprodcution才走优化项,其他时候想优化需要手动打开。

构建流程


1. 初始化参数

从配置文件和Shell语句中读取与合并参数,得出最终的参数;

webpack --config webpack.config.js --mode=production --env=production

mode指定环境为生成环境对应webpack.config.js中的modeproduction配置

2. 开始编译

用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译;

class Tapable{
    constructor(){
        this.hooks = []
    }
    //订阅事件
    tap(name,fn){
        this.hooks.push(fn);
    }
    //发布广播事件
    call(){
        this.hooks.forEach(hook=>hook((..arguments));
    }
    //webpack自带的模块Tapable帮我们实现发布者-订阅者模式
}

class Compiler extends Tapable{
    constructor(context){
        super();
        this.hooks={
            run:xxx(["compiler"]), //开始编译
            compilation:xxx(["compiler"]), //编译成compilation
            mask:xxx(["compilation"]), //创建好compilation对象,从Entry开始读取文件
            emit:xxx(["compiler"]),//输出文件
            done:xxx(["stats"]) //完成编译
            watchRun:xxx(["compiler"]) //开发时监听编译
        }
    }
}

上图是简化Compiler示例。

webpack执行时,会创建一个Compiler实例对象同时顺序广播一系列的事件--(this.hooks里面的事件类似于组件生命周期),广播的事件。

ps:plugin插件会作用在webpack打包过程中

3. 确认入口

根据配置中的entry找出所有的入口文件;

4. 编译模块

从入口文件出发,compilation会对所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖文件都经过了本步骤处理;

5. 完成模块编译

在经过第4步使用Loader翻译完所有模块后,得到每个模块被翻译后的最终内容以及它们之间的依赖关系; Loader翻译完的文件会先解析生成AST静态语法树(normalModuleLoader),分析文件的依赖关系require逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成__webpack_require__来模拟模块化操作。

6. 输出资源

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk 再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

modulechunk:module就是打包前import或require的js文件;chunk是打包后的文件即bundle.js,一个chunk可能包含多个module

7. 输出完成

在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果。

打包后的文件

打包后的main.js文件 因为浏览器默认不支持Commonjs模块规范,也就require方法无法使用,webpack自己实现了一个模块加载方法__webpack_require__

(function(modules) { // webpack 的启动代码自执行函数
  // The module cache 模块的缓存
  var installedModules = {};

  // The require function webpack自己实现了一个require方法
  function __webpack_require__(moduleId) {

    // Check if module is in cache 判断一下这个模块ID是否在缓存中
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;//如果有,说明此模块加载过,直接返回导出对象exports
    }
    // Create a new module (and put it into the cache)
    // 创建一个新的模块对象并且把它放到缓存中
    var module = installedModules[moduleId] = {
      i: moduleId,// 模块ID
      l: false,//是否已经加载loaded false
      exports: {} //导出对象,默认是一个空对象
    };

    // Execute the module function 执行此模块对应的方法,目的是给module.exports赋值
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded 把模块设置为已加载
    module.l = true;

    // Return the exports of the module 返回模块的导出对象
    return module.exports;
  }

  // the startup function
  function startup() {
    // Load entry module and return exports
    // 加载入口模块并且返回导出对象
    return __webpack_require__("./src/index.js");
  }

  // run startup 执行启动方法
  return startup();
})
({

 "./src/hello.js":
 (function(module) {
   module.exports = "hello";
 }),
 "./src/index.js":
 (function(__unusedmodule, __unusedexports, __webpack_require__) {
    let hello = __webpack_require__( "./src/hello.js");
    console.log(hello);
 })
});

上面模块删简化后

(function(modules) {})({
 "./src/hello.js":
 (function(module) {
   module.exports = "hello";
 }),
 "./src/index.js":
 (function(__unusedmodule, __unusedexports, __webpack_require__) {
    let hello = __webpack_require__( "./src/hello.js");
    console.log(hello);
 })
})
//最后简化是个IIFE,就是自执行函数咯,这个函数参数会包含一个对象,key为打包后模块路径,value为具体模块方法。

mian.js 能直接运行在浏览器中的原因在于输出的文件中通过 __webpack_require__ 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 main.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了对象中,执行一次网络加载。

如果仔细分析 __webpack_require__ 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

整个过程参考图

手写Loader

手写Plugin

什么抽象语法树AST

JavaScript Parser 会把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构。 转换成AST的目的就是将我们书写的字符串文件转换成计算机更容易识别的数据结构,这样更容易提取其中的关键信息,而这棵树在计算机上的表现形式,其实就是一个单纯的Object。

wepback打包递归遍历各个entry然后寻找依赖逐个编译的过程,每次递归都需要经历 String->AST->String 的流程。

webpack事件流怎么理解

我们将webpack事件流理解为webpack构建过程中的一系列事件,他们分别表示着不同的构建周期和状态,我们可以像在浏览器上监听click事件一样监听事件流上的事件,并且为它们挂载事件回调。

Table发布订阅模式怎么理解

参考文章

1.Webpack原理与实践(一):打包流程

2.Webpack揭秘——走向高阶前端的必经之路