什么是 webpack
webpack 是一个用于现代 JavaScript 应用程序的静态模块绑定器。当webpack处理你的应用程序时,它会在内部构建一个依赖关系图,映射项目所需的每个模块,并生成一个或多个包(bundles)。
webpack 可以转换打包多种类型的文件、模块、资源,包括 ES Modules 、CommonJS 和 AMD 模块, 也可以将 TypeScript 转换为 JavaScript,将 Handlebars 字符串转换为函数,将图片转换为 Base64,你也可以自己编写插件(plugins)来实现将任何你的应用程序需要的资源进行转换与打包。
webpack 支持所有兼容 ES5 的浏览器。webpack 需要 Promise 来 import() 和 require.ensure() ,在更老旧的浏览器中使用 webpack 时,需要使用 polyfill。
当前(2021年1月19日)最新版本的 webpack 是 v5.15.0, 以下内容默认适用于该版本, 另外 webpack5 运行时需要 Nodejs 版本在 10.13.0 以上。
(2021年12月2日)做了部分更新,当前最新版本的 webpack 是 v5.64.4。
webpack可以做什么
资源管理
将各种类型的文件/模块作为资源进行统一的处理,webpack 通过 资源视图 和 loader 来实现对项目资源的管理。
输出管理
通过丰富的插件,webpack 可以在打包过程中进行一系列的处理,同时支持多个入口和输出。
开发
区分 开发 模式和 生产 模式,可以对开发模式进行更有利于 debug 的编译(source map),热更新等。提供了 webpack-dev-server 本地简易服务器进行开发调试。
代码分割
支持将代码拆分为不同的包,然后可以按需或并行加载。它可以用来拆分更小的包和控制资源负载优先级,如果使用正确,会对负载时间产生重大影响。
多种方法可以实现代码分割,包括
- 使用 SplitChunksPlugin 插件将公用依赖导出到特定包
- 使用 mini-css-extract-plugin 来将 CSS 从主应用中拆分出来
- 使用动态 import
- 使用 预获取/预加载 (prefetch/preload)
- 包解析
- 使用官方提供的
webpack --profile --json > stats.json
生成分析的JSON文件 - webpack-chart
- webpack-visualizer
- webpack-bundle-analyzer
- webpack bundle optimize helper
- bundle-stats
- 使用官方提供的
缓存
浏览器使用缓存来避免无用的请求以及让资源加载更快,webpack 可以根据文件内容在输出文件名中添加 hash 串来控制浏览器的缓存更新策略。通常,可以将 node_modules 中的依赖打包为 vendors.js 然后固定 MODULE id,这样 vendors.[conenthash].js 便不会一直更新。
环境变量
通过设置 --env
参数设置环境变量,在不同的环境下执行不同的编译方案。
webpack的核心配置
Mode
通过将 mode 参数设置为 development 、 production 或 none,可以启用与每个环境相对应的 webpack 内置优化。默认值为 production 。
Entry
entry 指示 webpack 应该使用哪个模块来开始构建其内部依赖关系图。 webpack 将找出 entry 所依赖的其他模块和库(直接和间接)。
默认情况下,其值为 ./src/index.js
,但可以通过在 webpack 配置中设置 entry 属性来指定不同的(或多个入口点)。例如:
1 | module.exports = { |
Output
output 属性告诉 webpack 在何处导出它创建的包以及如何命名这些文件。对于主输出文件,默认为 ./dist/main.js
,对于任何其他生成的文件,默认为./dist
文件夹。
可以通过在配置中指定输出字段来配置流程的这一部分:
1 | const path = require('path'); |
Module Loaders
默认情况下, webpack 只理解 JavaScript 和 JSON 文件。Loaders 允许 webpack 处理其他类型的文件,并将它们转换为有效的模块,这些模块可以被应用程序使用并添加到依赖关系图中。
Loaders 是按照规则(rules)进行的,module 的 rules 是一个数组,其中每个 rule 在配置中有两个重要属性,test 属性标识应转换的文件,use 属性指示应该使用哪个加载程序进行转换。
Loaders可以是链式的,在链中的每个loader都会按照反向的顺序被执行。
1 | const path = require('path'); |
常用 loaders :
loader名字 | 作用 | 文件扩展名 |
---|---|---|
style-loader | 将 CSS 注入 DOM | .css |
css-loader | css-loader 将会像 import/require() 那样解析 @import 和 url() |
.css |
postcss-loader | 用于对css文件进行预处理,比如添加前缀 | .css |
file-loader | file-loader 将文件的 import/require() 解析为 url,并将文件发送到输出目录,在 webpack5 中使用自带的 asset/resource 来代替 |
.png .jpg .jpeg .gif |
url-loader | 将文件转换为 base64 url ,在 webpack5 中使用自带的 asset/resource 来代替 | .png .jpg .gif |
raw-loader | 允许导入文件作为字符串,在 webpack5 中使用自带的 asset/resource 来代替 | .txt |
csv-loader | 允许导入csv文件作为字符串 | .csv |
xml-loader | 允许导入xml文件作为字符串 | .xml |
less-loader | 将 less 文件编译为 css | .less |
sass-loader | 将 sass 文件编译为 css | .sass |
stylus-loader | 将 styl 文件编译为 css | .styl |
markdown-loader | 将 md 文件编译为 html | .md |
babel-loader | 用于对js文件进行预处理,例如将js文件中的语法进行兼容 | .js |
ts-loader | 将ts文件编译为js | .ts tsx |
html-loader | 将html文档以字符串形式导出 | .html |
Resolve
Resolve 决定了当导入一个模块时,模块应当如何被解析,最常用的用法是定义 import 模块的别名。
1 | module.exports = { |
Plugins
Loaders 用于转换某些类型的模块,但可以利用 plugins 执行更广泛的任务,如包优化、资源管理和环境变量的注入。
为了使用 plugin ,你需要先引入它并将其添加到配置文件的 plugins 数组中。大多数插件都可以通过选项进行定制。由于可以在配置中多次使用插件以实现不同的目的,因此需要通过使用 New 操作符调用插件来创建插件的实例。
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 引入的 plugin |
常用 plugins :
plugin 名字 | 作用 | 包 |
---|---|---|
webpack.BannerPlugin | 在每个生成的块的顶部添加一些信息,比如版权信息 | |
CleanWebpackPlugin | 编译前自动清除输出目录 | clean-webpack-plugin |
webpack.optimize.CommonsChunkPlugin | 提取块之间共享的公共模块 | |
CompressionWebpackPlugin | 准备资源的压缩版本,为其提供内容编码 | compression-webpack-plugin |
webpack.ContextReplacementPlugin | 重写require表达式的推断上下文 | |
CopyWebpackPlugin | 将单个文件或整个目录复制到生成目录 | copy-webpack-plugin |
webpack.DefinePlugin | 允许在编译时配置全局常量 | |
webpack.DllPlugin | 拆分捆绑包以显著缩短构建时间 | |
webpack.EnvironmentPlugin | 使用 DefinePlugin 的 process.env 更便捷的方法 | |
EslintWebpackPlugin | ESLint 的 webpack插件 | eslint-webpack-plugin |
webpack.HotModuleReplacementPlugin | 允许热替换(HMR) | |
HtmlWebpackPlugin | 为包创建HTML | html-webpack-plugin |
webpack.IgnorePlugin | 从包中排除某些模块 | |
webpack.optimize.LimitChunkCountPlugin | 设置分块的最小/最大限制以更好地控制分块 | |
webpack.optimize.MinChunkSizePlugin | 保持块大小高于指定的限制 | |
MiniCssExtractPlugin | 为每个需要CSS的JS文件创建一个CSS文件 | mini-css-extract-plugin |
webpack.NoEmitOnErrorsPlugin | 出现编译错误时不再抛出 | |
webpack.NormalModuleReplacementPlugin | 替换与 regexp 匹配的资源 | |
NpmInstallWebpackPlugin | 开发时自动安装缺失的依赖 | npm-install-webpack-plugin |
OccurrenceOrderPlugin | 通过模块调用次数给模块分配ids,常用的ids就会分配更短的id,使ids可预测,减小文件大小 | |
webpack.ProgressPlugin | 汇报编译进度 | |
webpack.ProvidePlugin | 使用模块而不必使用import/require | |
webpack.SourceMapDevToolPlugin | 支持对源映射进行更细粒度的控制 | |
webpack.EvalSourceMapDevToolPlugin | 支持对eval源映射进行更细粒度的控制 | |
TerserPlugin | 使用 Terser 压缩混淆项目中的JS | terser-webpack-plugin |
UglifyJsPlugin | js压缩插件 | uglifyjs-webpack-plugin |
WebpackBarPlugin | 优化webpack加载命令行界面为进度条格式 | progress-bar-webpack-plugin |
DevServer
DevServer 用来配置和 webpack-dev-server 相关的选项。
Externals
Externals 用来防止将某些 import 的包打包到 bundle 中,而是在运行时再去从外部获取这些扩展依赖。
webpack安装
使用npm/yarn安装
1 | npm install --save-dev webpack |
添加 build 脚本到 package.json 中
1 | "scripts": { |
配合使用的npm包
webpack webpack包
webpack-cli webpack工具包,从webpack4开始需要安装该包
webpack-bundle-analyzer webpack打包文件分析工具
webpack-dev-server webpack开发服务器
webpack-merge webpack配置文件合并工具,可以将两个配置文件合并为一个
哪些方法可以缩减webpack编译时间?
详见 build performance
以下提供了一些常见的方法
- 将加载程序应用到所需的最小模块数
module.rules[n].include
- 每个额外的加载程序/插件都有一个启动时间。尽量少用工具。
- 使用 DllPlugin 将更改较少的代码移动到单独的编译中
- 开发时在内存中编译 使用
webpack-dev-server
- 了解 devtool 选项的区别,大部分情况下使用
eval-cheap-module-source-map
- 开发时避免使用生产环境工具 例如 [contenthash]、TerserPlugin、AggressiveSplittingPlugin
- 勿使用 Node.js 8.9.10-9.11.1版本,这些版本的 Map Set 实现有性能问题
- 使用多线程编译 parallel-webpack & cache-loader
## webpack编译流程
- 初始化参数: 从配置文件 webpack.config.js 和 cli args 参数中读取参数 options 并进行合并
- 开始编译: 根据输入参数初始化 Compiler 对象,加载配置的插件 plugin,调用插件的的 apply 方法,执行 Compiler 对象的 run 方法开始执行编译(实例化 Compilation 对象)
- 确认入口: 根据配置中的 entry 找出所有的入口文件
- 编译模块: 从入口文件开始解析,调用配置的 loader 对不同类型的模块进行载入,递归这个过程直到所有的入口文件都经过处理,得到所有模块的依赖关系 Module Chain
- 输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk, 每个 chunk 生成一个资源列表 chunk assets, 之后会把多个 chunk 合并转换成单独的文件 bundle 加入到输出列表
- 输出完成: 根据 output 配置确定输出的路径和文件名,把文件内容写入到文件系统中
loader 和 plugin 的区别
- loader 是资源加载器,通过链式调用,loader 赋予了 webpack 加载不同类型资源的能力,例如解析 Less文件、解析图片资源等。
- plugin 是插件,通过在 webpack 编译过程中预先设置钩子函数,plugin 可以在 webpack 编译过程中实现各种各样的功能,例如打包优化、资源管理、环境变量注入等。
webpack 生命周期
- environment
- afterEnvironment
- entryOption
- afterPlugins
- afterResolvers
- initalize
- beforeRun
- run
- normalModuleFactory
- contextModuleFactory
- beforeComlile
- compile
- thisCompilation
- compilation
- make
- finishMake
- afterCompile
- shouldEmit
- emit
- afterEmit
- done
- afterDone
如何编写一个loader
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API,并通过 this 上下文访问。
- 一个 loader 只能传入一个参数,一个包含资源文件内容的字符串。
- loader 会返回一个或者两个值,第一个值的类型是 string 或者 Buffer 类型的数据,第二个可选值是 SourceMap。
- 如果是单个处理结果,可以在 同步模式 中直接
return
。如果有多个处理结果,则必须调用this.callback()
。 - 在 异步模式 中,必须调用
this.async()
来告知 loader runner 等待异步结果,它会返回callback
回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。 - loader 可以提供 pitch 方法来在 pitching 阶段执行一些操作,在 pitch 方法中返回数据可以跳过之后的 loader。
- loader 函数中的 this 可以访问 loader 上下文中的方法和属性
1 | import path from 'path'; |
如何自定义plugin
plugin 向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。
webpack 插件由以下组成:
- 一个 JavaScript 命名函数或 JavaScript 类。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
1 | // 一个 JavaScript 类 |
webpack Tree-shaking原理
- Tree-shaking 是一种通过清除多余代码的方式来优化项目打包体积的技术。
- Tree-shaking 利用了 ES6 模块的特点,ES6 的模块加载是静态的,因此整个依赖树可以在编译时被静态的推导出解析语法树(AST),从而在编译时删除未使用的部分
- webpack2 开始就已经支持 Tree-shaking 的特性,webpack4 中 mode 设置为 production 时默认开启 Tree-shaking
- 确保没有把 compiler 将 ES6 模块语法转换为 CommonJS 模块。这一块很重要,在你使用 babel-loader 或者 ts-loader 编译代码时,一定要保留 import 和 export。
- 如果打包的代码有副作用(Side Effects),并且需要导出,可以通过 package.json 的 sideEffect 属性来声明