Fork me on GitHub

深入探究Webpack原理

Webpack的介绍

webpack 是一个模块打包工具(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack的作用

  • 通过webpack可以将零散的js文件打包到一个文件中;
  • 通过webpack Loader实现对代码的编译转换,兼容低版本浏览器
  • 支持不同种类(js/css/img)的模块资源的打包
  • 具备代码拆分能力:所有模块按需分块打包,一般会把应用初次加载必须的模块打包到一起,其他模块单独打包,等到应用工作过程中再异步加载这个模块(渐进式加载就不用担心打包单文件过大而加载慢的问题)

核心概念

webpack的核心有以下几点:

  1. Entry:入口
  2. Output:出口
  3. Loader:加载器
  4. Plugin:插件
  5. Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。
  6. Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

具体可查阅:webpack安装使用

Loader和Plugin的区别

Loader 本质就是一个函数,主要用于对特殊类型资源的加载,转换输出成webpack识别的格式(接受文件作为参数,返回转化后的结构)。webpack自身只支持js和json这两种格式的文件,对于其他文件需要通过loader将其转换为commonJS规范的文件后,webpack才能解析到。

Plugin 就是插件,包含apply方法。apply方法会被 webpack的 compiler(编译器)对象调用,并且 compiler 对象可在整个 compilation(编译)生命周期内访问。

Plugin可以理解为扩展webpack,实现各种自动化构建任务。在webpack打包编译过程里,针对loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于webpack的事件机制(webpack在每一个工作环境都预留了合适的钩子),会监听webpack打包过程中的某些节点,挂载并执行任务。

开发一个Plugin

在插件项目中定义一个插件名类,在该类中通过compiler的hooks属性访问到xx钩子,再通过tap方法注册一个钩子函数(挂载要执行的函数在钩子上),返回处理后的新内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
///src/plugins/helloPlugin.js
// 命名函数。
function HelloPlugin(options) {
// 使用 options 设置插件实例……
console.log(options);
}
//插件函数的 prototype 上定义一个 apply 方法
HelloPlugin.prototype.apply = compiler => {
//hooks
compiler.hooks.done.tap('HelloPlugin', status => {
console.log(status.toJson());
})
// 设置回调来访问 compilation 中的步骤:
compilation.plugin('optimize', () => {
console.log('optimize');
})
}

module.exports = HelloPlugin
实现loader

loader 的处理策略:

  • CSS:转换成 js 的模块,执行模块会在 DOM 中创建 style 标签并且插入CSS内容
  • Img:转换成图片路径
  • JSON:转化成 js 模块, default export = json

loader的执行顺序是从右到左,从下到上。而loader的加载顺序是从左到右,从上到下。

babel-loader的主要原理:
调动@babel/core这个包下面的transform方法,将源码通过presets预设来进行转换,然后生成新的代码、map和ast语法树传给下一个loader。这里的presets,比如@babel/preset-env这个预设其实就是各类插件的集合,基本上一个插件转换一个语法,比如箭头函数转换,有箭头函数转换的插件,这些插件集合就组成了预设。

1
2
3
4
5
6
// myloader替换尖括号
module.exports = function(source){
var content = "";
content = source.replace("/[<>]/g","尖括号");
return content;
}

webpack钩子

  • beforeRun: 在编译器执行前
  • run: 在编译器开始读取记录前执行
  • compilation: 创建compilation后
  • beforeCompile: 在编译前
  • compile: 创建compilation前
  • make: 编译完成前
  • done: 编译完成后
  • emit: 文件提交到dist目录前
  • afterEmit: 文件提交到dist目录后

source map配置

在webpack配置文件中,指定devtool: “source-map”等字段可以输出对应的.map文件,方便在浏览器中调试,sourceMap对应的模式有很多,建议:

  • 开发环境:cheap-module-eval-source-map,报错可以定位到源码的行信息,且构建速度比较快
  • 生产环境: nosources-source-map,可以定位到报错的位置,且不会暴露源代码

webpack的特性

  • HMR(热更新)

  • Tree-shaking(摇树):生产环境自动启动,打包时去掉没有用到的代码成员

    可以通过配置webpack的 optimization 对象控制:

    • usedExports 打包结果中只导出外部用到的成员;
    • minimize 压缩打包结果,去掉无效代码
  • sideEffects:开启可以打包时无视无效模块中的副作用代码(需要在package.json中同时配置)

  • code splitting:通过把项目资源模块按照我们的规则打包到不同的bundle中,降低应用成本,提高响应速度

    webpack实现分包方式:

    • 根据业务不同配置多个打包入口,输出多个打包结果(适用于传统多页应用)
    • 结合ES Module的动态导入特性,按需加载模块(import(module path).then())

webpack的扩展方式

loader、plugin、minimizer

构建流程/打包流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化:
    • 初始化参数:启动打包流程,读取并合并配置参数
    • 创建编译器对象:载入webpack核心模块,传入配置项,创建Compiler对象
    • 开始编译:使用Compiler对象开始编译项目
  • 编译:
    • 确定入口:从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
    • 编译模块:将每个模块交给Loader;对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为JS模块文件(对于无法被转成js的资源文件,Loader一般会将它们单独作为资源文件拷贝到输出目标中,将资源文件的路径作为导出成员)
  • 输出:将编译后的模块组合成 Chunk,将 Chunk 转换成文件,输出到文件目录中

PS:
编译的过程中,webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。所以Plugin应该在webpack生命周期开始之前注册,才能监控到事件节点。

打包原理

webpack通过模拟module,exports,require变量,将我们的模块代码打包成一个IIFE(立即执行函数),,函数参数是我们写的各个模块被包装之后组成的数组,浏览器执行这个立即执行函数就可以运行我们的模块代码。

  1. 根据入口文件,先逐级递归识别依赖,构建依赖图谱
  2. 将代码转化成AST抽象语法树
  3. 在AST阶段中处理代码
  4. 把AST抽象语法树变成浏览器可以识别的代码, 然后输出

打包优化

常见的优化就是拆包、分块、压缩等,主要是两个优化点:

  1. 减少打包时间
  2. 减少打包大小

减少打包时间

  1. 优化解析时间:开启多进程打包
  • thread-loader(webpack4 官方推荐):把这个 loader 放置在其他 loader 之前, 之后的 loader 就会在一个单独的 worker 池里运行,一个worker 就是一个nodeJS 进程。
  • HappyPack(将不再维护):将loader的同步执行转为并行,减少loader的编译等待时间
  1. 合理利用缓存
  • 缓存已编译过的文件(如设置babel-loader的cacheDirectory)
  • cache-loader:对性能开销较大的 loader 使用。可以将使用了该loader的结果缓存到磁盘里,显著提升二次构建速度
  1. 优化压缩时间:webpack4内置默认使用terser-webpack-plugin 插件压缩优化代码。
  2. 优化搜索时间:缩小文件搜索范围 减小不必要的编译工作
  • 优化 loader 文件搜索范围:使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件
  • 优化resolve下的配置

减少打包大小

  • 启用gzip压缩
  • 按需加载:首页加载文件越小越好,将每个页面单独打包为一个文件
  • 在webpack4中production下打包默认开启UglifyJS压缩代码
  • 开启Tree-shaking等去掉无效代码

热更新(HMR)

热更新(Hot Module Replacement)是指能够不用刷新浏览器而将新变更的模块替换掉旧的模块(不用刷新整个页面)。热更新实现主要分为几部分功能:

  • 服务器构建、推送更新消息
  • 浏览器模块更新
  • 模块更新后页面渲染

原理:

  • 通过webpack-dev-server创建两个服务器:提供静态资源的服务(express)和Socket服务
  • 使用 webpack-dev-server 托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码;
  • 浏览器加载页面后,与 WDS 建立 WebSocket 连接
  • Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash 事件
  • 浏览器接收到 hash 事件后,请求 manifest 资源文件,确认增量变更范围
  • 浏览器加载发生变更的增量模块
  • Webpack 运行时触发变更模块的 module.hot.accept 回调,执行代码变更逻辑

简单理解:dev server 启动以后,会 watch 源文件的变化。当源文件发生变化后,Webpack 会重新编译打包, 再通过 ws 连接通知浏览器去获取新的打包文件,然后对页面做局部更新。

webpack和rollup的选择

Rollup 是一个 JavaScript 模块打包器(ES Module打包器),和webpack类似,但小巧的多,它可以将小块代码编译成整块代码,从而使得这些模块更好的运行在浏览器或nodeJS环境。Rollup中只能通过插件扩展。

Rollup优点:

  • 输出结果更加扁平,执行效率更高
  • 良好的tree-shaking, 自动移除未引用代码
  • 打包结果可读

Rollup缺点:

  • 如要加载非ES Module第三方模块会比较复杂
  • 模块最终会打包到全局,不能HMR
  • 浏览器环境中,代码拆分模块需要用require.js这样的AMD库

webpack 拆分代码, 按需加载;Rollup 所有资源放在同一个地方,一次性加载,利用 tree-shake 特性来剔除项目中未使用的代码,减少冗余。

Rollup偏向应用于js库、框架;webpack偏向应用于应用开发。如果应用场景中只是js代码,希望做ES转换,模块解析,可以使用Rollup。如果应用场景中涉及到css、html,涉及到复杂的代码拆分合并,建议使用webpack。

组件库选择rollup: webpack 无法构建出 esm 格式的 js 文件。 即使借助一些插件实现了,产出代码也比不上 rollup 简洁、干净。

gulp和webpack

Glup和Webpack区别

webpack5和webpack4区别

  • webpack5通过优化 Tree Shaking 和代码生成来减小Bundle体积(更好的处理嵌套 tree-shaking)

  • 压缩代码:
    webpack4需要安装terser-webpack-plugin 插件

    webpack5: 内置了 terser-webpack-plugin 插件,在 mode=“production” 的时候会自动开启 js 压缩功能

    1
    2
    3
    4
    5
    6
    7
    // webpack.config.js中
    module.exports = {
    optimization: {
    usedExports: true, //只导出被使用的模块
    minimize : true // 启动压缩
    }
    }
  • 缓存设置:
    webpack5 内部内置了 cache 缓存机制, cache 会在开发模式下被设置成 type: memory 而且会在生产模式把cache 给禁用掉

    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    module.exports= {
    // 使用持久化缓存
    cache: {
    type: 'filesystem'
    cacheDirectory: path.join(__dirname, 'node_modules/.cac/webpack')
    }
    }
  • 启动服务:
    webpack4通过 webpack-dev-server 启动服务

    webpack5内置使用 webpack serve 启动

  • 模块依赖关系构建

  • 打包体积更小

-------------完结撒花 -------------

本文标题:深入探究Webpack原理

文章作者:咕噜咕噜

发布时间:2022年07月15日

最后更新:2022年10月16日

原始链接:https://aartemida.github.io/2022/07/15/深入探究Webpack原理/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。