打包原理

Webpack 把解析的所有模块变成一个对象,然后通过入口模块去加载我们的东西,然后依次实现递归的依赖关系,通过入口来运行所有的文件。Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

魔术注释(Magic Comment)

Webpack 提供了一种特殊的魔术注释(Magic Comment)功能,允许开发者为动态导入的模块指定特定的名字,以方便 Webpack 在编译过程中进行优化和分割。
这种魔术注释的形式是在 import 语句中添加下面这样的注释,其中 `chunk-name` 是要给模块指定的名字。

/* webpackChunkName: chunk-name */

例如:

import(/* webpackChunkName: "vendors" */ './vendors.js');

这段代码会告诉 Webpack 把从 `./vendors.js` 导入的模块命名为 `vendors`,并在生成最终的输出文件时按照这个名字来进行分割和合并。
注意,如果在多个不同的地方都导入了同一个模块,并且为它提供了不同的名字,那么 Webpack 将会选择一个最常见的名字作为最终的输出文件名。如果没有任何名字是最常见的,则 Webpack 将使用默认的命名规则。
此外,除了 `webpackChunkName`,Webpack 还支持其他的魔术注释,比如 `webpackPrefetch` 和 `webpackPreload`,分别用于标记一个模块是否应该被预加载或预读取。

关键词解释

chunk:

chunk代表的是webpack构建时将代码分割成小块进行处理的结果

chunk是由webpack根据模块的依赖关系进行生成和管理的。是webpack本身就具备代码分割的功能,可以通过配置entryoutput等字段来实现。

它通常被用来加载异步组件或分割大型的代码库。可以有效地减少代码的冗余和加载时间。

vendor:

指的是存放第三方库或者其他外部资源的文件夹。它会在node_modules文件夹中查找任何需要的第三方依赖项。这些资源可能包括vue、vue-router、axios、better-scroll等,相对于我们自己开发的应用程序来说,这些都是第三方的。

在Vue项目中,vendor的生成是通过webpack的内置splitChunks插件自动处理的。当webpack编译应用程序代码时,它会在node_modules文件夹中查找任何需要的第三方依赖项,并将它们打包到一个名为“vendor”的文件中。

bundle:

bundle代表的是最终项目打包生成的文件,它包含了程序所有的chunk和模块。在webpack执行构建时,会把每个chunk打包成一个bundle文件。这些bundle文件是经过压缩和优化的,以便在浏览器中加载和运行时能够提供更好的性能。

代码分割的本质和意义

用可接受的服务器性能压力增加来换取更好的用户体验

  • 源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
  • 打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。

Webpack 的构建流程

初始化参数:

从配置文件(如 `webpack.config.js`)和和 Shell 语句中读取并合并参数。
这些参数包括入口文件、输出路径、模块规则、插件等。

开始编译:

使用上一步得到的参数来初始化一个 `Compiler` 对象。
加载所有配置的插件,这些插件会在整个构建过程中监听特定事件,并在合适的时机执行相应的任务。

执行对象的 run 方法开始执行编译。

确定入口:

根据配置中的 `entry` 属性找到入口文件。
从入口文件开始解析代码,构建抽象语法树(AST),找出依赖关系。

编译模块:

遍历每个模块,根据文件类型和 loader 配置调用对应的 loader 来处理文件内容。
如果模块有依赖其他模块,则递归地对这些依赖进行相同的处理。
编译后的模块会被存储起来,等待下一步的处理。

生成依赖图:

将编译后的模块按照其依赖关系连接起来,形成一个完整的依赖图。

优化阶段:

在这个阶段,Webpack 可能会执行一系列的优化操作,例如代码分割、压缩、摇树(tree-shaking)、懒加载等。
插件也可以在这个阶段发挥作用,进行额外的优化工作。

生成资源:

根据模块和依赖图生成最终的输出文件。

组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
输出的文件可能是 JavaScript 文件、CSS 文件、HTML 文件或其他类型的静态资源。

输出完成:

根据配置中的输出路径和文件名,将生成的资源写入到文件系统中。

清理旧的资源:

如果启用了清除旧资源的选项,Webpack 还会在这个阶段删除不再需要的旧文件。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。此外,Webpack 也支持多进程和多线程的构建模式,以提高构建速度。

简单说:

初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

常见的Loader

raw-loader:加载文件原始内容(utf-8)
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
svg-inline-loader:将压缩后的 SVG 内容注入代码中
image-loader:加载并且压缩图片文件
json-loader:加载 JSON 文件(默认包含)
handlebars-loader: 将 Handlebars 模版编译成函数并返回
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
sass-loader:将SCSS/SASS代码转换成CSS
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
babel-loader:把 ES6 转换成 ES5
ts-loader: 将 TypeScript 转换成 JavaScript
awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
eslint-loader:通过 ESLint 检查 JavaScript 代码
tslint-loader:通过 TSLint检查 TypeScript 代码
vue-loader:加载 Vue.js 单文件组件
mocha-loader:加载 Mocha 测试用例的代码
coverjs-loader:计算测试的覆盖率
i18n-loader: 国际化
cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?

可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行。(inline 官方不推荐使用)

plugins(插件)

内置插件:作为Webpack的静态属性存在。例如,DefinePlugin是用来定义全局常量的插件,BannerPlugin可以给每个chunk生成的文件头部添加一行注释,通常用于添加作者、公司、版权等信息。

第三方插件

CommonsChunkPlugin

CommonsChunkPlugin 是 Webpack 的一个内置插件,它可以将多个入口点之间的共享代码拆分出来,形成一个单独的共享代码块(也称为 common chunk 或 manifest chunk)。这样做的好处是可以减少网络请求的数量,提高网页的加载速度。
使用 CommonsChunkPlugin 的基本步骤如下:

 1. 配置插件

接下来,在 Webpack 配置文件中添加 CommonsChunkPlugin 插件的配置信息。下面是一个简单的示例:

const path = require('path');

module.exports = {
ᅠ entry: {
ᅠ ᅠ app: './src/app.js',
ᅠ ᅠ vendor: ['lodash', 'react']
ᅠ },
ᅠ output: {
ᅠ ᅠ filename: '[name].[chunkhash].bundle.js',
ᅠ ᅠ path: path.resolve(__dirname, 'dist')
ᅠ },
ᅠ optimization: {
ᅠ ᅠ splitChunks: {
ᅠ ᅠ ᅠ cacheGroups: {
ᅠ ᅠ ᅠ ᅠ commons: {
ᅠ ᅠ ᅠ ᅠ ᅠ test: /[\\/]node_modules[\\/]/,
ᅠ ᅠ ᅠ ᅠ ᅠ name: 'vendor',
ᅠ ᅠ ᅠ ᅠ ᅠ chunks: 'all'
ᅠ ᅠ ᅠ ᅠ }
ᅠ ᅠ ᅠ }
ᅠ ᅠ }
ᅠ },
ᅠ plugins: [
ᅠ ᅠ new webpack.optimize.CommonsChunkPlugin({
ᅠ ᅠ ᅠ name: 'manifest'
ᅠ ᅠ })
ᅠ ]
};

在这个示例中,我们有两个入口点 `app` 和 `vendor`。`vendor` 入口点包含了一些外部库,如 lodash 和 react。我们在优化部分设置了 splitChunks 参数,并指定了一个 cacheGroups,要求 Webpack 将所有依赖于 node_modules 的模块提取出来放到 vendor 编译出来的文件中。同时在 plugins 部分使用了 CommonsChunkPlugin 将所有 entry 点的公用代码提取出来放入名为 'manifest' 的 js 文件中。

2. 执行 Webpack

最后,运行 Webpack 编译命令即可生成拆分后的代码块。在上面的示例中,将会生成三个输出文件:app.[chunkhash].bundle.js、vendor.[chunkhash].bundle.js 和 manifest.[chunkhash].bundle.js。

splitChunks

splitChunks是webpack内置的。它是一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件。

define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
ignore-plugin:忽略部分文件
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
serviceworker-webpack-plugin:为网页应用增加离线缓存功能
clean-webpack-plugin: 目录清理
uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前),webpack提速
terser-webpack-plugin: 支持压缩 ES6 (Webpack4),webpack提速
webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
ModuleConcatenationPlugin: 开启 Scope Hoisting,webpack提速
speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时),webpack提速
webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

Loader和Plugin的区别

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

加载顺序:从后到前,从右往左。

Plugin 就是插件,其本质是监听整个打包的生命周期,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

加载顺序:从上到下,从左到右。

编写loader的思路

Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。

编写loader看官网api

  • Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
  • Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
  • 尽可能的异步化 Loader,如果计算量很小,同步也可以
  • Loader 是无状态的,我们不应该在 Loader 中保留状态
  • 使用 loader-utils 和 schema-utils 为我们提供的实用工具
  • 加载本地 Loader 方法
    • Npm link
    • ResolveLoader

编写Plugin的思路

webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

  • compiler 暴露了和 Webpack 整个生命周期相关的钩子
  • compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
  • 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
  • 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
  • 找出合适的事件点去完成想要的功能
    • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
    • watch-run 当依赖的文件发生变化时会触发
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

编写plugin看官网api

使用webpack开发时,你用过哪些可以提高效率的插件

webpack-dashboard:

可以更友好的展示相关打包信息。

webpack-merge:

提取公共配置,减少重复配置代码

speed-measure-webpack-plugin:

简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。

size-plugin:

监控资源体积变化,尽早发现问题

HotModuleReplacementPlugin:

模块热替换

如何优化 Webpack 的构建速度

多进程/多实例构建:

HappyPack(不维护了)、thread-loader

多进程并行压缩

webpack-paralle-uglify-plugin(不再维护)
uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
terser-webpack-plugin 开启 parallel 参数(支持ES6)

DLL:

使用 DllPlugin 进行对第三方库分包提前打包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,通过 json 文件告诉webpack这些库提前打包好了,避免反复编译浪费时间。
HashedModuleIdsPlugin 可以解决模块数字id问题

充分利用缓存提升二次构建速度:

babel-loader 开启缓存
terser-webpack-plugin 开启缓存
使用 cache-loader 或者 hard-source-webpack-plugin

缩小构建目标/减少文件搜索范围:

exclude(不需要被解析的模块)/include(需要被解析的模块)
resolve.modules 告诉 webpack 解析模块时搜索的目录,指明第三方模块的绝对路径
resolve.mainFields 限定模块入口文件名,只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
resolve.alias 当从 npm 包中导入模块时(例如,import * as React from ‘react’),此选项将决定在 package.json 中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同
resolve.extensions 尽可能减少后缀尝试的可能性
noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
IgnorePlugin (完全排除模块)

动态Polyfill

通过 Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)

Scope hoisting (「作用域提升」)

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 把引入的 js 文件“提升到”它的引入者顶部,其实现原理为:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。
必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法和 Scope Hoisting 要分析模块之间的依赖关系,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法

提取页面公共资源:

使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件基础包分离

Tree shaking

purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
更多优化请参考官网-构建性能

参考大佬文章「吐血整理」再来一打Webpack面试题 

更多推荐