Webpack详解

475 阅读11分钟

模块化

产生原因

  • 大量模块成员污染全局作用域
  • 没有私有空间,模块内成员可以被模块外成员访问修改
  • 模块较多时,容易产生命名冲突
  • 无法管理模块间的依赖关系
  • 维护过程中也很难辨别成员所属模块

commonJS

CommonJS规范,是Node.js中所遵循的模块规范。约定:一个文件是一个模块,每个模块有单独的作用域。通过module.exports导出,通过require引入。

CommonJS 是以同步的方式加载模块,因为Node.js执行机制是在启动时加载模块,执行时使用。但并不适用于浏览器,如果浏览器使用同步加载,会引发大量的同步模式请求,导致应用运行效率低。

AMD

AMD是每个模块通过define() 函数去定义。如果当前模块中需要向外部导出成员,可以通过return的方式实现。require() 函数用于自动加载模块。可以实现异步加载模块。

require.js是基于AMD规范实现的库,主要实现原理是需要加载一个模块是,内部就会自动创建script标签去请求并执行响应模块的代码。

    // ADM 定义一个模块
    define(['A', 'B'], function(moduleA, moduleB) {
        return moduleC
    })
    
    // ADM 引入一个模块
    require(['C'], function(moduleC) {
        moduleC.init
    })

CMD

CMD类似于CommonJS规范,具体实现有Sea.js, 后面Require.js 兼容了。同步加载模块。

    define(function(require, exports,module) {

        // 通过require 引入依赖
        var A = require('moduleA')

        // 通过exports 或者 module.exports对外暴露成员
        module.exports = function () {
            return B
        }
    })

ES Module

支持可按需导入单独模块,支持异步导入。通过import导入, export、export default 导出。

    import A form B
    import { default as A} form B

    export default A


    import {a,b,c} from C
    import * as D from C

    export {a,b,c}

将import()作为函数调用,将其作为参数传递给模块的路径。 它返回一个 promise,所以可以异步加载。

    import('/modules/myModule.mjs')
      .then((module) => {
        // Do something with the module.
      });

Webpack 核心特性

Webpack 想要实现的是整个前端项目的模块化,项目中的各种资源(包括 CSS 文件、图片等)都应该属于需要被管理的模块。换句话说, Webpack 不仅是 JavaScript 模块打包工具,还是整个前端项目(前端工程)的模块打包工具。也就是说,我们可以通过 Webpack 去管理前端项目中任意类型的资源文件。

在日常开发中我们对打包方案或设想是:

  • 可以兼容用户浏览器
  • 能够将散落的模块打包到一起,避免浏览器频繁发送网络请求
  • 支持不同种类的前端资源模块

Tips

  1. webpack.config.js 是一个运行在Node.js环境中的JS文件,所以我们需要按照CommonJS的方式编写代码。使用module.export。
  2. 在开发过程中使用VS code 时,可以在头部加上import { Configuration } from 'webpack' 用来只能提示webpack 配置项设置
  3. VS Code 折叠代码

在例子中简单打包后会生成bundle.js,bunde.js 是一个自执行函数,内含会加载模块数组,以及定义工具方法如webpack.require 用来加载其他模块等。

Loader

webpack 中默认只能处理JS模块代码,在打包过程中默认将所有文件都当作JS 代码处理。Webpack 实现不同种类资源模块加载的核心就是 Loader。

CSS

css-loader: css-loader 只会把 CSS 模块加载到 JS 代码中,而并不会使用这个模块。

style-loader: 将 css-loader 中所加载到的所有样式模块,通过创建 style 标签的方式添加到页面上。

所以在配置过程中需要按照执行顺序配置,从后往前,从右往左依次配置。

如何编写一个loader

Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。

  1. 每个loader 是一个独立的模块,引入需要处理的文件内容,输出处理完成的内容。
  2. 处理每类文件最后的loader需要在这个 Loader 的最后返回一段 JS 代码字符串。
  3. 找到一个合适的加载器,在后面接着处理我们得到的结果。

插件Plugins

Webpack 插件机制的目的是为了增强 Webpack 在项目自动化构建方面的能力。

插件常见应用场景

  • 实现自动在打包之前清除 dist 目录(上次的打包结果); clean-webpack-plugin
  • 自动生成应用所需要的 HTML 文件;html-webpack-plugin
  • 根据不同环境为代码注入类似 API 地址这种可能变化的部分;
  • 拷贝不需要参与打包的资源文件到输出目录;copy-webpack-plugin
  • 压缩 Webpack 打包完成后输出的文件;提取css在单独的文件:mini-css-extract-plugin 压缩样式文件: optimize-css-assets-webpack-plugin 内置的 JS 压缩插件:terser-webpack-plugin
  • 自动发布打包结果到服务器实现自动部署;ssh2-sftp-client
  • 分析打包后体积:webpack-bundle-analyzer 模块代码缓存:hard-source-webpack-plugin 并行压缩: `terser-webpack-plugin

如何编写一个Plugin

Webpack 的插件机制就是我们在软件开发中最常见的钩子机制。

钩子机制也特别容易理解,它有点类似于 Web 中的事件。在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。

webpack钩子

那么plugin的核心就是明确任务的执行时机,并确定应该把这个任务挂载到哪个钩子上。

Tips

  1. npx可以调用项目内部安装的模块,npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
  2. 可以使用npm的 process.env.npm_config_xxx 来获取当前执行的命令行参数XXX变量 npm之config (也可以使用yargs来获取命令行参数)

webpack 核心步骤

  • 通过 Loader 处理特殊类型资源的加载,例如加载样式、图片;

  • 通过 Plugin 实现各种自动化的构建任务,例如自动压缩、自动发布。

    Webpack 启动后,会根据我们的配置,找到项目中的某个指定文件(一般这个文件都会是一个 JS 文件)作为入口。然后顺着入口文件中的代码,根据代码中出现的 import(ES Modules)或者是 require(CommonJS)之类的语句,解析推断出来这个文件所依赖的资源模块,然后再分别去解析每个资源模块的依赖,周而复始,最后形成整个项目中所有用到的文件之间的依赖关系树。

    有了这个依赖关系树过后, Webpack 会遍历(递归)这个依赖树,找到每个节点对应的资源文件,然后根据配置选项中的 Loader 配置,交给对应的 Loader 去加载这个模块,最后将加载的结果放入 bundle.js(打包结果)中,从而实现整个项目的打包。

webpack cli 启动打包流程

  • 找到入口文件获取命令行参数: webpack-cli 的入口文件 bin/cli.js 中,会通过 yargs 模块解析 CLI 参数,所谓 CLI 参数指的就是我们在运行 webpack 命令时通过命令行传入的参数,例如 --mode=production;
  • 合并默认参数和配置参数: 调用了 bin/utils/convert-argv.js 模块,将得到的命令行参数转换为 Webpack 的配置选项对象。在 convert-argv.js 工作过程中,首先为传递过来的命令行参数设置了默认值,然后判断了命令行参数中是否指定了一个具体的配置文件路径,如果指定了就加载指定配置文件,反之则需要根据默认配置文件加载规则找到配置文件。找到配置文件过后,将配置文件中的配置和 CLI 参数中的配置合并,如果出现重复的情况,会优先使用 CLI 参数,最终得到一个完整的配置选项;
  • 开始载入 Webpack 核心模块,传入配置选项,创建 Compiler 对象,这个 Compiler 对象就是整个 Webpack 工作过程中最核心的对象了,负责完成整个项目的构建工作。

image.png

webpack 开始构建

  • 完成 Compiler 对象的创建过后,紧接着这里的代码开始判断配置选项中是否启用了监视模式
    • 如果是监视模式就调用 Compiler 对象的 watch 方法,以监视模式启动构建,但这不是我们主要关心的主线。
    • 不是监视模式就调用 Compiler 对象的 run 方法,开始构建整个应用。
  • run方法就是先触发了beforeRun 和 run 两个钩子,然后最关键的是调用了当前对象的 compile 方法,真正开始编译整个项目
  • compile 方法内部主要就是创建了一个 Compilation 对象,Compilation 字面意思是“合集”,实际上,你就可以理解为一次构建过程中的上下文对象,里面包含了这次构建中全部的资源和信息。
  • 创建完 Compilation 对象过后,紧接着触发了一个叫作 make 的钩子,进入整个构建过程最核心的 make 阶段。

make 阶段

make 阶段主体的目标就是:根据 entry 配置找到入口模块,开始依次递归出所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同的 Loader 处理。

Webpack 的插件系统是基于官方自己的 Tapable 库实现的,我们想要知道在哪里注册了某个事件,必须要知道如何注册的事件。Tapable 的注册方式具体如下:

image.png

所以,我们只需要通过开发工具搜索源代码中的 make.tap,就应该能够找到事件注册的位置。

  1. SingleEntryPlugin (单一入口打包方式) 中调用了 Compilation 对象的 addEntry 方法,开始解析入口;
  2. addEntry 方法中又调用了 _addModuleChain 方法,将入口模块添加到模块依赖列表中;
  3. 紧接着通过 Compilation 对象的 buildModule 方法进行模块构建;
  4. buildModule 方法中执行具体的 Loader,处理特殊资源加载;
  5. build 完成过后,通过 acorn 库生成模块代码的 AST 语法树;
  6. 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环 build 每个依赖;
  7. 所有依赖解析完成,build 阶段结束;
  8. 最后合并生成需要输出的 bundle.js 写入 dist 目录。

webpack 开发配置技巧

Dev Server 本地开发

需要满足以下条件:

  1. 使用Http 服务运行,更接近生产环境
  2. 修改代码后,webpack 自动完成构建,浏览器及时显示最新结果
  3. 提供source map,便于更方便更快速定位问题

webpack-dev-server

运行 webpack-dev-server 这个命令时,它内部会启动一个 HTTP Server,为打包的结果提供静态文件服务,并且自动使用 Webpack 打包我们的应用,然后监听源代码的变化,一旦文件发生变化,它会立即重新打包。

webpack-dev-server 为了提高工作速率,它并没有将打包结果写入到磁盘中,而是暂时存放在内存中,内部的 HTTP Server 也是从内存中读取这些文件的。这样一来,就会减少很多不必要的磁盘读写操作,大大提高了整体的构建效率。

devtool

模块热替换机制(HMR)

指的是我们可以在应用运行过程中,实时的去替换掉应用中的某个模块,而应用的运行状态不会因此而改变。

并不是开箱即用,需要额外增加一些代码。vue.js HMR

Tree-shaking

Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能:

  • usedExports - 打包结果中只导出外部用到的成员;
  • minimize - 压缩打包结果。
module.exports = {
  // ... 其他配置项
  optimization: {
  
    // 模块只导出被使用的成员
    usedExports: true,
    
    // 压缩输出结果
    minimize: true,
    
    
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    
  }
}

Code Splitting(分块打包)

webpack 实现分包方式:

  • 根据业务不同配置多个打包入口,输出多个打包结果, 提取公共模块splitChunks
  • 结合 ES Modules 的动态导入(Dynamic Imports)特性,按需加载模块。

Tips

  • 用多个文件配置时,简单的使用Object.assgin 无法满足合并配置需求,可以安装webpack-merge
  • Define Plugin配置全局变量

好文推荐

滴滴webpack系列

webpack5+Ts实践篇

webpack知识点集合

webpack编译流程