Loader & Plugin
Loader & Plugin 区别
Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
Loader
文档: webpack.docschina.org/concepts/lo…
api: www.webpackjs.com/api/loaders…
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
- 处理一个文件可以使用多个loader,loader的执行顺序是和本身的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行。
- 第一个执行的loader接收源文件内容作为参数,其他loader接收前一个执行的loader的返回值作为参数。最后执行的loader会返回此模块的JavaScript源码
loader 遵循以下原则
| 标题 | |
|---|---|
| 单一职责原则 | 每一个 Loader 只负责一种转换任务,不涉及其他的逻辑处理 |
| 纯函数原则 | 每个 Loader 都应该是一个纯函数,输入参数相同,输出结果也相同 |
| 可组合原则 | Loader 可以链式调用,多个 Loader 组合使用,实现复杂的转换任务 |
| 最小化原则 | 尽量避免在 Loader 中执行复杂耗时的操作,如 I/O 操作和计算密集型任务,保持 Loader 的轻量化和高效性。 |
| 易用性原则 | Loader 应该易于使用和配置,提供友好的 API 和详细的文档说明。 |
- loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
- loader 可以是同步的,也可以是异步的。
- loader 运行在 Node.js 中,并且能够执行任何操作。
- loader 可以通过
options对象配置(仍然支持使用query参数来设置选项,但是这种方式已被废弃)。 - 除了常见的通过
package.json的main来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用loader字段直接引用一个模块。 - 插件(plugin)可以为 loader 带来更多特性。
- loader 能够产生额外的任意文件。
可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言转译(或编译)和
module: {
rules: [
{
// 这个规则表示对所有扩展名为.js的文件,使用babel-loader进行转换处理,并排除掉node_modules目录下的文件。
test: /.js$/,
use: 'babel-loader',
exclude: /node_modules/
}
]
}
自定义一个loader
module.exports = function(source) {
return source.toLowerCase();
};
module: {
rules: [
{
test: /.txt$/,
use: './loaders/my-loader.js'
}
]
}
// 使用 Loader Options
module: {
rules: [
{
test: /.txt$/,
use: {
loader: './loaders/my-loader.js',
options: {
capitalize: true
}
}
}
]
}
// 使用 `this.query` 来访问这些选项Loader Options
module.exports = function(source) {
if (this.query.capitalize) {
return source.toUpperCase();
} else {
return source.toLowerCase();
}
};
常见的loader
配置方式&解释:webpack.docschina.org/concepts/lo…
v5: webpack.docschina.org/loaders/
v4: v4.webpack.docschina.org/loaders/
val-loader将代码作为模块执行,并将其导出为 JS 代码ref-loader用于手动建立文件之间的依赖关系
cson-loader加载并转换 CSON 文件
babel-loader使用 Babel 加载 ES2015+ 代码并将其转换为 ES5esbuild-loader加载 ES2015+ 代码并使用 esbuild 转译到 ES6+buble-loader使用 Bublé 加载 ES2015+ 代码并将其转换为 ES5traceur-loader使用 Traceur 加载 ES2015+ 代码并将其转换为 ES5ts-loader像加载 JavaScript 一样加载 TypeScript 2.0+coffee-loader像加载 JavaScript 一样加载 CoffeeScriptfengari-loader使用 fengari 加载 Lua 代码elm-webpack-loader像加载 JavaScript 一样加载 Elm
html-loader将 HTML 导出为字符串,需要传入静态资源的引用路径pug-loader加载 Pug 和 Jade 模板并返回一个函数markdown-loader将 Markdown 编译为 HTMLreact-markdown-loader使用 markdown-parse 解析器将 Markdown 编译为 React 组件posthtml-loader使用 PostHTML 加载并转换 HTML 文件handlebars-loader将 Handlebars 文件编译为 HTMLmarkup-inline-loader将 SVG/MathML 文件内嵌到 HTML 中。在将图标字体或 CSS 动画应用于 SVG 时,此功能非常实用。twig-loader编译 Twig 模板并返回一个函数remark-loader通过remark加载 markdown,且支持解析内容中的图片
style-loader将模块导出的内容作为样式并添加到 DOM 中css-loader加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码less-loader加载并编译 LESS 文件sass-loader加载并编译 SASS/SCSS 文件postcss-loader使用 PostCSS 加载并转换 CSS/SSS 文件stylus-loader加载并编译 Stylus 文件
vue-loader加载并编译 Vue 组件angular2-template-loader加载并编译 Angular 组件
plugin 插件
文档: www.webpackjs.com/concepts/pl…
api 钩子:www.webpackjs.com/api/compile…
compiler
compilation
plugin 是 webpack 的支柱功能。webpack在运行的生命周期中会广播出许多事件(基于发布订阅模式实现的),Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
plugin 是扩展器,它丰富了 webpack 本身,针对 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制(Tapable)工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。
作用: 对开发者来说就是可以接触到 webpack 构建流程中的各个阶段并劫持做一些代码处理,对使用者来说则是我们可以通过各类插件实现诸如自动生成 HTML 模版 (html-webpack-plugin)、自动压缩图片 (imagemin-webpack-plugin) 等功能。
创建一个插件的流程
1. 创建插件类
开发者需要创建一个插件类,并实现 apply 方法。apply 方法是插件被 Webpack 调用时调用的方法,它接收一个 compiler 参数,该参数提供了很多 Webpack 编译过程中的钩子函数,可以在这些钩子函数中执行一些自定义操作。
2. 监听 Webpack 事件
开发者需要在插件类中注册一些 Webpack 事件监听器,以便在编译过程中执行一些自定义操作。Webpack 提供了很多事件钩子,例如 compilation、emit 等等。其中 compilation 事件是最常用的事件之一,它会在每次编译后触发,并提供了很多编译过程中的信息和对象。
3. 执行自定义逻辑:
在监听到 Webpack 事件之后,开发者可以执行一些自定义逻辑,例如修改生成的文件内容、生成额外的文件或者进行文件压缩等等。
4. 导出插件: 最后,开发者需要将插件导出,以便 Webpack 可以正确地加载和使用该插件。
class MyPlugin {
// 在模块中导出一个具有 apply 方法的 JavaScript 类。apply 方法在 webpack 编译过程中被调用。
apply(compiler) {
// 监听 webpack 的事件 下面是构建完成事件
compiler.hooks.done.tap('MyPlugin', () => {
console.log('Webpack build is done!');
alert('Webpack build is done!');
});
// 用 webpack 的钩子系统监听指定的事件,例如 `emit`(生成输出文件之前),并在适当的时候执行插件的行为。
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 在这里编写插件逻辑
callback();
});
}
}
module.exports = MyPlugin;
// 删除.js文件中的表情符
class RemoveEmojiPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('RemoveEmojiPlugin', (compilation, callback) => {
Object.keys(compilation.assets).forEach(assetName => {
if (/\.js$/.test(assetName)) {
let asset = compilation.assets[assetName];
let source = asset.source().toString();
source = source.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '');
asset.source = () => source;
compilation.assets[assetName] = asset;
}
});
callback();
});
}
}
module.exports = RemoveEmojiPlugin;
提高开发效率的插件
- webpack-dashboard:可以更友好的展示相关打包信息。
- webpack-merge:提取公共配置,减少重复配置代码
- speed-measure-webpack-plugin:打包速度分析工具、简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
size-plugin:监控资源体积变化,尽早发现问题HotModuleReplacementPlugin:模块热替换webpack-dev-server:这是一个快速的服务器应用程序,可用于在本地主机上启动 Webpack 开发服务器。它提供了热重载和自动刷新功能,允许您在文件更改时快速重新加载。clean-webpack-plugin:此插件可帮助您删除以前生成的文件。这对于确保您始终有最新版本的代码非常有用。html-webpack-plugin:此插件可自动生成 HTML 文件,并将编译后的 JavaScript 自动注入到文件中。这减少了手动操作,使您更加专注于编写代码。webpack-bundle-analyzer:此插件可帮助您分析 Webpack 打包后的结果并识别潜在的性能问题。它提供了一个交互式的图形用户界面,展示出打包后的模块大小等信息。mini-css-extract-plugin:此插件允许您从 JavaScript 中提取 CSS 代码,并将其保存为单独的文件。这样可以减少页面加载时间,并提高性能。copy-webpack-plugin:此插件可帮助您复制文件或目录到输出路径中。这可以用于将静态资源,例如字体和图像,一起打包。
常见插件plugin
配置&解释:webpack.docschina.org/concepts/pl…
v5: webpack.docschina.org/plugins/
v4: v4.webpack.docschina.org/plugins/
BannerPlugin为每个 chunk 文件头部添加 bannerCommonsChunkPlugin提取 chunk 之间的公共模块用以共享CompressionWebpackPlugin准备好用 Content-Encoding 格式传送的压缩版资源包ContextReplacementPlugin重写 require 表达式的推断上下文CopyWebpackPlugin复制某个文件或整个文件夹到生成目录中DefinePlugin允许创建一个在编译时可配置的全局常量DllPlugin拆分 bundles,从而大幅减少构建时间EnvironmentPlugin在process.env键上使用DefinePlugin的简写方式EslintWebpackPluginwebpack 的 ESLint 插件HotModuleReplacementPlugin启用热更新 (HMR)HtmlWebpackPlugin快速创建 HTML 文件来服务 bundlesIgnorePlugin从 bundles 包中移除某些模块LimitChunkCountPlugin设置 chunk 的最小/最大限制,以便更好的控制 chunkMinChunkSizePlugin确保 chunk 大小在指定限制之上MiniCssExtractPlugin为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件NoEmitOnErrorsPlugin|出现编译错误时,跳过输出阶段NormalModuleReplacementPlugin替换与正则表达式匹配的资源NpmInstallWebpackPlugin开发时自动安装缺少的依赖ProgressPlugin报告编译进度ProvidePlugin使用模块但无需使用 import/requireSourceMapDevToolPlugin对 source map 进行更细颗粒度的控制EvalSourceMapDevToolPlugin对 eval source map 进行更细颗粒度的控制TerserPlugin|在你的项目中使用 Terser 插件来压缩 JS
修改 Loader & Plugin 的执行顺序
要修改webpack中loader或者plugin的执行顺序,可以通过以下两种方式来实现:
修改 Loader的执行顺序
1. 使用enforce属性
每个loader都有一个enforce属性,它可以用来控制loader的执行顺序。默认情况下,所有的loader按照配置文件中声明的顺序依次执行。但是,如果某个loader的enforce属性为"pre",那么这个loader会在其他loader之前执行;如果enforce属性为"post",则会在其他loader之后执行。
例如,我们想要让eslint-loader在babel-loader之前执行,可以把eslint-loader的enforce属性设置为"pre":
javascript复制代码
module.exports = {
module: {
rules: [
{
enforce: 'pre',
test: /.js$/,
exclude: /node_modules/,
loader: 'eslint-loader'
},
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}
};
2. 使用数组语法
除了使用enforce属性之外,还可以使用数组语法来修改loader的执行顺序。在配置文件中,可以把多个loader放到一个数组里面,并且按照需要的顺序排序。Webpack会依次执行数组中的loader,直到处理完所有的loader。
例如,我们想要让sass-loader在css-loader之前执行,可以这样写:
javascript复制代码
module.exports = {
module: {
rules: [
{
test: /.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
};
修改Plugin的执行顺序
要修改plugin的执行顺序,可以使用webpack内置的tapable库。Webpack在运行时会创建一个全局的EventEmitter对象,通过这个对象来触发不同的事件。
Webpack钩子(Hook)是一种事件,它可以让用户在不同的阶段中插入自定义的代码。通过利用钩子,我们可以在不同的阶段中添加自己的处理逻辑。
例如,我们想要让clean-webpack-plugin在html-webpack-plugin之前执行,可以这样写:
javascript复制代码
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*', '!index.html']
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
};
// 将clean-webpack-plugin放到第一个位置
const [firstPlugin, secondPlugin] = config.plugins;
config.plugins = [secondPlugin, firstPlugin];
Webpack构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口:根据配置中的 entry 找出所有的入口文件编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
简单说
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
对源码感兴趣的同学可以移步我的另一篇专栏从源码窥探Webpack4.x原理
source map是什么?生产环境怎么用?
source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
hidden-source-map:借助第三方错误监控平台 Sentry 使用nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)
1、 在 Webpack 配置文件中添加以下代码来启用 sourceMap 选项
javascript复制代码
module.exports = {
// ...
devtool: 'source-map',
// ...
}
2、 使用 devtool 选项配置需要的 sourceMap 类型。常见的选项有
eval: 生成代码后立即执行,并使用 eval() 执行,最终生成的 Source Map 不会写入磁盘。速度较快。source-map: 生成单独的文件用于 Source Map,适合生产环境使用。cheap-source-map: 生成 Source Map 但不包含列信息,适合开发环境使用。cheap-module-source-map: 与 cheap-source-map 相似,但同时会将 loader 的 Source Map 加入进来。
3、 如果你使用了 CSS 或者其他样式表,需要设置使用 sourceMap 的选项,例如,在 css-loader 中需要设置 sourceMap: true
javascript复制代码
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
{
loader: 'style-loader',
options: { sourceMap: true }
},
{
loader: 'css-loader',
options: { sourceMap: true }
}
]
}
]
}
};
4 确保在构建时输出 sourceMap 文件,可以通过 output 选项进行配置
javascript复制代码
module.exports = {
output: {
// ...
devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]',
sourceMapFilename: '[file].map',
},
};
这样就可以在生产环境中启用和使用 sourceMap 了。记住要在生产环境发布前禁用 sourceMap,以避免暴露源代码。
模块打包原理
文件监听原理
Webpack 的文件监听原理是基于 Node.js 的 fs 模块提供的 fs.watch() 和 fs.watchFile() 方法。Webpack 监听文件变化的主要过程如下:
-
当启动时,Webpack 会遍历整个项目文件系统,建立一个文件依赖关系图,并将其存储在内存中。
-
在文件系统中发生变化时,例如某个文件被修改、删除或添加,
fs.watch()方法会向操作系统注册一个文件监听器,并在文件发生变化时通知 Webpack。 -
当 Webpack 接收到文件变化的通知后,它会重新构建需要更新的模块和依赖模块的代码,并生成新的输出文件。
-
最后,Webpack 会将新的输出文件发送给浏览器,使得应用程序可以及时地反映出最新的代码变化。
需要注意的是,由于文件监听会不断地监测文件系统,因此可能会对系统资源产生一定的负载。为了减小负载,Webpack 提供了一些优化策略,如缓存和轮询等机制,以提高监听效率和减少系统开销。
文件监听开启方式
- 启动 webpack 命令时,带上 --watch 参数
- 在配置 webpack.config.js 中设置 watch:true
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000,
},
}
热更新原理(HMR)
详细讲解: zhuanlan.zhihu.com/p/30669007
基本原理
Webpack HMR 的基本原理是通过 Websocket 建立一个服务端和客户端之间的连接。服务端监听源代码文件的变化,并将变化的模块打包成 chunk 文件,然后通过 Websocket 将这些 chunk 文件推送到客户端。客户端接收到新的 chunk 后,会根据 module ID 进行热更新,更新后的代码立即生效,不需要刷新页面。
流程说明
Webpack HMR 的流程可以分为三个阶段:构建阶段、监视阶段和更新阶段。
1、构建阶段
在构建阶段,Webpack 会生成一份 manifest 文件,这个文件记录了每个模块对应的 ID,在后续热更新时使用。同时,Webpack 也会在输出的 bundle.js 文件中添加一些 HMR 相关的代码,用于处理热更新逻辑。
javascript复制代码
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
上面的代码配置了入口文件和输出文件的名称,并且使用了 webpack.HotModuleReplacementPlugin() 插件来启用 HMR 功能。这个插件会在构建阶段向输出的 bundle.js 文件中添加 HMR 相关的代码。
2、监视阶段
在监视阶段,Webpack 会监听源代码文件的变化,并将变化的模块打包成 chunk 文件,然后通知客户端进行热更新。Webpack 使用了一种叫做 "jsonp" 的技术,来实现客户端接收新代码的功能。具体流程如下:
- 客户端与服务端建立 Websocket 连接。
- 客户端向服务端发送一个 HMR 请求,请求获取当前模块的 hash 值。
- 服务端返回当前模块的 hash 值,并开始监听源代码文件的变化。
- 当有模块发生变化时,服务端会生成一个新的 chunk 文件,并将该文件的 hash 值和变化的模块 ID 发送给客户端。
- 客户端收到新的 chunk 后,根据模块 ID 找到需要更新的模块,然后使用新的代码替换旧的代码,从而完成热更新。
3、更新阶段
在更新阶段,客户端会根据模块 ID 找到需要更新的模块,并使用新的代码替换旧的代码。如果这个模块依赖了其他模块,那么Webpack还会递归地更新这些模块。最后,客户端会通知应用程序,告诉它更新完成。
注意事项
- 由于 HMR 是一种开发工具,因此应该只在开发环境中启用。在生产环境中使用 HMR 可能会导致安全问题和性能问题。
- 不是所有的模块都支持 HMR,只有一些特定的模块才被设计成可以热更新的。如果你写的模块不支持 HMR,那么在修改代码后,仍然需要手动刷新浏览器来查看效果。
- 如果你的代码包含错误,Webpack 可能无法进行热更新。在这种情况下,你需要手动刷新浏览器来恢复到上一个稳定版本。
如何对bundle体积进行监控和分析?
VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。
bundlesize 工具包可以进行自动化资源体积监控。
Hash 文件指纹
Webpack 文件指纹是 Webpack 生成的文件名中包含的哈希值,用于标识文件内容的唯一性。
Hash:每一次构建时都会生成一个唯一的 Hash 值。主要应用于解决浏览器缓存问题,通常用于给文件名添加后缀。例如,filename: '[name].[hash].js'可以将输出的文件名设置为bundle.821ebc3f9b16a2cd6f51.js,当文件内容发生变化时,Hash 值就会改变,从而让浏览器重新加载最新的资源。ChunkHash:根据 chunk 内容计算出的 Hash 值。每个 chunk 都有自己的 Hash 值,如果只修改了某个 chunk 的内容,那么只有该 chunk 对应的 Hash 值才会发生变化,其他 chunk 不受影响。主要应用于多页面应用,可以针对每个页面生成独立的入口文件和对应的 Hash 值。例如,filename: '[name].[chunkhash].js'可以将输出的文件名设置为bundle.b430e4b13b1e47e12f74.js,当某个 chunk 内容发生变化时,只有该 chunk 对应的 Hash 值会改变,从而让浏览器重新加载该页面的资源。ContentHash:根据文件内容计算出的 Hash 值。如果只修改了文件的内容,那么对应的 ContentHash 值才会发生变化,其他文件不受影响。主要应用于缓存优化,在使用了长时间缓存的前提下,可以通过 ContentHash 实现在文件内容发生变化时自动更新缓存。例如,filename: '[name].[contenthash].js'可以将输出的文件名设置为bundle.3a9d0bacd5b6ec4c7d44.js,当文件内容发生变化时,对应的 ContentHash 值就会改变,从而让浏览器重新加载最新的资源。ModuleHash:根据模块的相对路径和代码内容计算出的 Hash 值。如果某个模块的代码内容或者位置发生变化,那么对应的 ModuleHash 值才会发生变化,其他模块不受影响。主要应用于开发环境下的缓存优化,在使用了热更新(Hot Module Replacement)的前提下,可以通过 ModuleHash 实现只重新加载发生变化的模块。例如,filename: '[name].[modulehash].js'可以将输出的文件名设置为bundle.9e761994094a4b2c35df.js,当某个模块的代码内容发生变化时,对应的 ModuleHash 值就会改变,从而让开发服务器只重新加载该模块。
js、css、图片指纹配置
{
entry: './src/index.js',
// js 指纹配置
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
// css 指纹配置
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
// 图片指纹配置
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]',
outputPath: 'images/',
publicPath: 'images/',
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
};
如何优化 Webpack 的构建速度?
serverless-action.com/fontend/web…
代码分割 Code Splitting
官方文档:webpack.docschina.org/guides/code…
背景:
在实际开发中,我们通常会面临如下的问题:
- 代码体积过大,导致页面加载速度缓慢。
- 应用程序的初始加载时间过长,影响用户体验。
- 部分模块只在特定情况下才被使用,但是仍然被打包进最终的文件中。
意义:
为了解决这些问题,Webpack 提供了一种叫做代码分割(Code Splitting)的技术,可以将应用程序的代码拆分成各个小块,从而减少应用程序的初始加载时间和整体代码体积。
-
加速初始加载时间:将应用程序的代码拆分成多个小块,可以减少应用程序的初始加载时间,从而提高用户体验。
-
减少代码体积:通过将应用程序的代码拆分成多个小块,并且根据不同的场景进行动态加载,可以减少应用程序的整体代码体积,从而提高应用程序的性能。
-
提高缓存效果:通过将应用程序的代码拆分成多个小块,可以提高缓存效果,从而减少服务器的压力,提高应用程序的性能。
1、 基于入口点(Entry Points)的代码分割
这是最常见的代码分割方式,通过配置 webpack 的 entry 选项来指定多个入口文件。每个入口文件将生成一个单独的 bundle。这种方法最简单、易于理解,适用于小型应用程序。
优点:
- 简单易懂
- 适用于小型应用程序
缺点:
- 无法处理公共模块,可能会导致重复加载
- 难以管理大型应用程序 Webpack 的默认行为是将所有依赖打包到一个文件中。但是,你可以通过配置entry属性来告诉Webpack 分别打包不同的模块。
module.exports = {
entry: {
index: './src/index.js',
vendor: './src/vendor.js'
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
}
};
上面的示例中,Webpack 将会生成两个不同的 bundle 文件,一个是用于应用程序的 index.js,另外一个是用于第三方库的 vendor.js。这种方法适用于简单的项目或者只有少量代码需要分离的场景。
2、防止重复
如果在应用程序中使用了很多相同的代码,例如某个工具库或者UI组件,你可以考虑将这些代码提取出去,避免重复打包。
复制代码
module.exports = {
entry: {
index: './src/index.js'
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 2
}
}
}
}
};
上面的示例中,Webpack 将会检查应用程序代码中是否存在重复代码,并将这些代码提取到 commons.js 中。
3、基于动态导入(Dynamic Imports)的代码分割
这种方式通过使用 ES6 的 import() 函数来实现动态导入模块。当使用该模块时,webpack 将自动为其创建一个新的 bundle。此方法适用于异步加载场景,例如点击链接或滚动页面等事件触发。
优点:
- 可以实现按需加载,减少初始加载时间
- 可以处理公共模块,避免重复加载
缺点:
- 只适用于异步加载场景
- 需要额外的配置和代码
通过使用 import() 方法来实现动态导入。这种方法适用于需要按需加载模块的场景,例如异步路由、懒加载组件等。使用此方式分割的代码会组成一个单独的 chunk 文件。
async function getComponent() {
const result = await import(/* webpackChunkName: "lodash" */ 'lodash');
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
getComponent().then(component => {
document.body.appendChild(component);
});
上面的示例中,import() 方法根据传入的参数异步地加载模块。注释 /* webpackChunkName: "lodash" */ 是为该chunk文件设置名称,以便在构建过程中更好地调试和识别。
4、基于 SplitChunksPlugin 的代码分割
SplitChunksPlugin 是 webpack 内置插件,用于自动将公共模块提取到单独的 bundle 中。该插件可以通过配置 optimization.splitChunks 对象来指定哪些模块应该被提取。
优点:
- 可以自动处理公共模块,避免重复加载
- 适用于大型应用程序
缺点:
- 需要额外的配置和代码
- 可能会导致过多的 HTTP 请求
Babel原理
Babel是一个广泛使用的JavaScript编译器,可以将ES6+代码转换为向后兼容的JavaScript版本,以便在旧版浏览器或环境中使用。以下是Babel的详细原理:
- 解析器: Babel首先使用解析器来解析输入的代码。解析器读取代码并将其转换为AST(抽象语法树)表示形式。这个AST将被用于在下一步进行转换。
- 转换器:接下来,Babel使用一组插件集合,它们被称为转换器。每个转换器都会检查AST,并根据需要修改它。例如,如果您使用了ES6的箭头函数,转换器将把它转换为ES5标准的函数。
- 生成器:一旦完成所有转换,Babel需要将AST转换回代码。因此,在生成器阶段,Babel将AST遍历并生成输出代码。
- 运行时支持:有些语言特性,如类和模块,无法通过简单的转换来实现。为了使这些功能在旧版环境中正常工作,Babel提供了一个运行时库。当Babel转换代码时,它添加了对该库的引用,以确保代码可以正确地运行。
Taro就是利用 babel 完成的小程序语法转换