- 基本处理
{
test: /\.js$/,
use: {
loader: 'bable-loader',
options: {
preset: ['@babel/preset-env']
}
}
}
- 处理 Promise 等高级特性
preset: [
[
"@babel/preset-env",
{
/**
* false: 默认值,无视浏览器兼容配置,引入所有 polyfill
* entry: 根据配置的浏览器兼容,引入浏览器不兼容的 polyfill
* usage: 根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加
*/
useBuiltIns: "false | entry | usage",
},
],
];
- 支持 class 装饰器写法
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
];
- 借助 'ts-loader' 或 '@babel/preset-typescript'
- vue-loader:用于 Vue 单文件组件的 webpack 加载器
- vue-template-compiler:将 Vue 2.0 模板预编译为渲染函数(template => ast => render)
- @babel/preset-react: JSX 被编译为 React.createElement 函数调用
// webpack.config.js
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: "./src/index",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
// 1. 声明 webpack 的构建目标为 library
library: {
name: "npm-library-name",
type: "umd",
},
},
// 2. 引入第三方包时让 webpack 不要一起打包
externals: [nodeExternals()],
// 3. 生产 Sourcemap
devtool: "source-map",
};
// package.json
{
// 4.1 声明 npm 包名
"name": "npm-library-name",
// 4.2 指定 ES Module 模式引入时的入口文件
"module": "src/index.js",
// 4.3 指定 ES 其它 模式引入时的入口文件
"main": "dist/main.js",
"scripts": {
// 5. 在发布前自动执行编译命令
"prepublishOnly": "webpack --mode=production"
}
}
-
webpack5 模块联邦(MF)
- 将单个巨大应用拆分成多个小应用形式
- 各个应用能够独立开发(不同应用允许使用不同技术栈)、独立部署
- 与其它微前端方案不同,MF 的应用之间关系平等,没有主应用/子应用之分
-
Monorepo 单软件包
- 多个 repo 里的代码现在就放在一个 repo,便于管理
- Monorepo 中所有模块都共享, 公共依赖只要安装一次
- 微前端应用适合 Monorepo 这种代码组织方式
-
Webpack5 Module Federation + pnpm-workspace 实现微前端
-
pnpm-workspace
- 初始化
mkdir micro-frontend cd micro-frontend pnpm init
- 初始化工作空间:在根目录下创建 pnpm-workspace.yaml 文件
packages: - 'packages/**'
- 安装依赖
pnpm i webpack -D -W // 所有packages共享 pnpm i lodash -S -r --filter app1 // 只安装在 app1 下 pnpm i lodash -S // 只安装在 app1 下,也可以直接在 app1 目录下安装
-
Webpack5 MF
- 模块导出
// app1/webpack.config.js const { ModuleFederationPlugin } = require("webpack").container; module.exports = { plugins: [ new ModuleFederationPlugin({ name: "app1", // app1 应用名称 fielname: "app1.js", // app1 模块入口 expose: { // 定义app1导出哪些模块 "./utils": "./src/utils", "./foo": "./src/foo", }, // 依赖共享 shared: ["lodash"], }), ], // MF 应用资源提供方必须以 http(s) 形式提供服务 devServer: { port: 8081, }, };
- 模块导入
// app2/webpack.config.js const { ModuleFederationPlugin } = require("webpack").container; module.exports = { plugins: [ new ModuleFederationPlugin({ // 使用 remotes 属性引入远程模块列表 remotes: { // 地址需要指向导出方生成的应用入口文件 /** * app1和上面的name对应 * http://localhost:8081和上面的devServer对应 * app1.js和上面的fielname对应 */ RemoteApp1: "app1@http://localhost:8081/dist/app1.js", }, // 依赖共享 shared: ["lodash"], }), ], }; // app2/src/index.js (async () => { /** * RemoteApp1和上面的remotes.RemoteApp1对应 * utils和expose.utils对应 */ const { sayHello } = await import("RemoteApp1/utils"); sayHello(); })();
-
- 处理:Asset Module 模型(指定 module.rules.type),无需 loader
type = "asset/resource"
: 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现type = "asset/inline"
: 导出一个资源的 data URI。之前通过使用 url-loader 实现type = "asset/source"
: 导出资源的源代码。之前通过使用 raw-loader 实现type = "asset"
: 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现
- 优化
- 图片压缩: 借助 image-webpack-loader
- 雪碧图:借助 webpack-spritesmith
- 响应式图片:借助 responsive-loader
- Webpack 内置 stats 接口,专门用于统计模块构建耗时、模块依赖关系等信息
// webpack.config.js
module.exports = {
// ...
// 添加 profile = true 配置
profile: true,
};
npx webpack --json=stats.json
- SpeedMeasureWebpackPlugin: 统计出各个 Loader、插件的处理耗时
// yarn add -D speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const config = {};
module.exports = smp.wrap(config);
- UnusedWebpackPlugin: 查找出工程项目里哪些文件没有被用到
// yarn add -D unused-webpack-plugin
const UnusedWebpackPlugin = require("unused-webpack-plugin");
module.exports = {
plugins: [
new UnusedWebpackPlugin({
directories: [path.join(__dirname, "src")], // 指定需要分析的文件目录
root: path.join(__dirname, "../"), // 用于指定根路径,与输出有关
}),
],
};
- webpack4 持久化缓存方案:cache-loader || hard-source-webpack-plugin
// 借助 `cache-loader`
// 1. 将 Loader 处理结果保存到硬盘,下次运行时若文件内容没有发生变化则直接返回缓存结果
// 2. 只缓存了 Loader 执行结果,缓存范围与精度不如 Webpack5 内置的缓存功能,所以性能效果相对较低(性能提升在 60% ~ 80% 之间)
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
use: ["cache-loader", "babel-loader", "eslint-loader"],
},
],
},
//...
};
// 借助 `hard-source-webpack-plugin`
// 1. 不仅仅缓存了 Loader 运行结果,还保存了 Webpack 构建过程中许多中间数据
// 2. 底层逻辑与 Webpack5 的持久化缓存很相似,但优化效果稍微差一些(性能提升在 62% ~ 88% 之间)
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin");
module.exports = {
// ...
plugins: [new HardSourceWebpackPlugin()],
};
- webpack5 持久化缓存方案:内置 cache 方案,效果优于上面两种
- webpack5 将首次构建出的 Module、Chunk、ModuleGraph 等对象序列化后保存到硬盘中
- 在下次编译时对比每一个文件的内容哈希或时间戳,未发生变化的文件跳过编译操作,直接使用缓存副本,减少重复计算
module.exports = {
cache: {
type: "filesystem", // 缓存类型,支持 'memory' | 'filesystem',需要设置为 filesystem 才能开启持久缓存
cacheDirectory: '', // 缓存文件路径,默认为 node_modules/.cache/webpack
buildDependencies: { // 额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为各种配置文件
config: [
path.join(__dirname, 'webpack.dll_config.js'),
path.join(__dirname, '.babelrc')
],
}
maxAge: '', // 缓存失效时间,默认值为 5184000000
},
};
- Webpack4 之前的项目,可以使用 HappyPack 实现并行文件加载
// yarn add -D happypack
rules: [
{
test: /\.js$/,
// 1. 使用 happypack/loader 代替原本的 Loader 序列
use: 'happypack/loader?id=js',
}
],
// 2. 使用 HappyPack 插件注入代理执行 Loader 序列的逻辑
plugins: [
new HappyPack({
id: 'js',
loaders: ['babel-loader']
})
]
- Webpack4 之后则建议使用 Thread-loader
// yarn add -D thread-loader
rules: [
{
test: /\.js$/,
use: ["thread-loader", "babel-loader"],
}
],
- HTML:html-webpack-plugin minify 选项配置 || 使用 html-minifier-terser
- CSS:optimize-css-assets-webpack-plugin + cssnano
- JS:Webpack4 默认使用 Uglify-js 实现代码压缩,Webpack5.0 默认使用 Terser 作为 JavaScript 代码压缩器
- 图片:使用 image-webpack-loader
-
webpack 的分包机制
- Initial/Entry Chunk:entry 模块及相应子模块打包成 Initial Chunk
- 多少个 entry 就多少个 Chunk, 一个 entry 若引用了多个 module, 这些 moudle 都会被分配到该 entry 对应的 chunk 中
- Async Chunk:通过 import('./xx') 等语句导入的异步模块及相应子模块组成的 Async Chunk
- Runtime Chunk:entry.runtime 不为空时,会将运行时模块单独组织成一个 Chunk
- 根据 splitChunks 设定创建若干 Chunk 对象
- Initial/Entry Chunk:entry 模块及相应子模块打包成 Initial Chunk
entry: {
entry1: { import: "./entry1", runtime: "solid-runtime" },
entry2: { import: ".//entry2", runtime: "solid-runtime" },
},
splitChunks: {
cacheGroups: {
lodash: {
test: /lodash/,
name: "lodash",
minChunks: 1,
},
}
}
// entry1.js
import common from "./common";
import("./async-module");
import lodash from 'lodash'
// entry2.js
import common from './common'
// 1. Initial Chunk
{chunk[entry1]:module[entry1.js], module[common.js]}
{chunk[entry2]:module[entry2.js], module[common.js]}
{chunk[lodash]: module[node_modules/lodash/lodash.js]}
// 2. Async Chunk
{chunk[async-module]: module[async-module.js]}
// 3. Runtime Chunk
{chunk[solid-runtime]: ……}
-
分包的必要性
- 模块重复打包:假如多个 Chunk 同时依赖同一个 Module,那么这个 Module 会被不受限制地重复打包进这些 Chunk
- 资源冗余:客户端必须等待整个应用的代码包都加载完毕才能启动运行,但可能用户当下访问的内容只需要使用其中一部分代码
- 缓存失效:将所有资源达成一个包后,所有改动 —— 即使只是修改了一个字符,客户端都需要重新下载整个代码包,缓存命中率极低
-
如何分包:使用 webpack 内置的 SplitChunks(主要有两种类型的配置)
- minChunks/minSize/maxInitialRequest 等分包条件,满足这些条件的模块都会被执行分包
- cacheGroup :用于为特定资源声明特定分包条件,例如可以为 node_modules 包设定更宽松的分包条件
-
最佳分包策略
- 将 node_modules 模块打包成单独文件(通过 cacheGroups 实现)
- 针对业务代码,通过 minChunks 配置项将频繁使用的资源合并为 common 资源,单独打包
- 首屏用不上的代码,尽量以异步方式引入
optimization: {
chunks: 'all', // 'all' | 'async' | 'initial'
minChunks: 2, // Module 被使用频率, 它并不直接等价于被 import 的次数,而是取决于上游调用者是否被视作 Initial Chunk 或 Async Chunk 处理
maxInitialRequests: 5, // Initial Chunk 最大并行请求数
axAsyncRequests: 3, // Async Chunk 最大并行请求数
cacheGroups: {
// 将 node_modules 模块打包成单独文件
vendors: {
test: /[\\/]node_modules[\\/]/,
minChunks: 1,
},
}
}
- CSS 摇树:purgecss-webpack-plugin
- JS 摇树:设置
optimization: {useExports: true}
module.exports = {
mode: "production",
optimization: {
usedExports: true,
},
};
- 在 Webpack 进入构建阶段后,首先会通过 IO 接口读取文件内容
- 之后调用
LoaderRunner
并将文件内容以source
参数形式传递到 Loader 数组 - source 数据在 Loader 数组内可能会经过若干次形态转换,最终以标准 JavaScript 代码提交给 Webpack 主流程,以此实现内容编译功能
- 基本方法
- 在 webpack 配置文件中定义“自定义 loader”的查找路径:
resolveLoader: {modules: ['node_modules', './custom-loader/']}
- Loader 通常是一种 mapping 函数形式,接收原始代码内容,返回编译结果:
module.exports = function (source) {return modifySource;}
- Loader 运行过程可以通过上下文接口,有限制地影响 Webpack 编译过程,上下文接口将在运行 Loader 时以 this 方式注入到 Loader 函数
- 获取传给 loader 的参数:
this.getOptions()
- 返回多个结果:
this.callback(null, result)
- 处理 loader 里的异步事件:
this.async(null, result)
- 取消 Loader 缓存:
this.cacheable(false)
- 在 Loader 中直接写出文件(例如 file-loader 依赖该接口写出 Chunk 之外的产物):
this.emitFile()
- 添加额外的文件依赖,当这些依赖发生变化时,会触发重新构建:
this.addDependency()
- 获取传给 loader 的参数:
- 在 webpack 配置文件中定义“自定义 loader”的查找路径:
- 校验 loader 参数:借助
schema-utils
- 日志处理 -- 使用 Loader Context 的 getLogger 接口:
const logger = this.getLogger("xxx-loader"); logger.info("information")
- 上报异常
- 一般使用
logger.error
,仅输出错误日志,不会打断编译流程 - 对于需要明确警示用户的错误,优先使用
this.emitError
接口,同样不会打断编译流程 - 对于已经严重到不能继续往下编译的错误,使用
this.callback
接口提交错误信息,,效果与直接使用 throw 相同,会打断编译流程:this.callback(new Error("发生了一些异常"))
- 一般使用
- webpack 插件是一个带有
apply
函数的类,constructor(ops)
中的 opts 参数可以获取传给插件的参数 - Webpack 在启动时会调用插件对象的 apply 函数,并以参数方式传递核心对象
compiler
- compiler 对象可以触发多种钩子 Hook (Webpack5 暴露了多达 200+ 个 Hook),表示在 webpack 构建过程的哪个阶段执行插件方法,比如:
compiler.hooks.compilation
:Webpack 刚启动完,创建出 compilation 对象后触发(参数:当前编译的 compilation 对象)compiler.hooks.make
:正式开始构建时触发(当前编译的 compilation 对象)compilation.hooks.onEmit
:完成代码构建与打包操作,准备将产物发送到输出目录之前触发(当前编译的 compilation 对象)ompiler.hooks.done
:编译完成后触发(stats 对象)
- Hook 有多种调用方式,比如
tap
:当钩入到编译(compile) 阶段时,只有同步的 tap 方法可以使用tapAsync/tapPromise
:对于可以使用 AsyncHook 的 run 阶段, 则需使用 tapAsync 或 tapPromise(以及 tap)方法
- 在 Webpack 运行过程中,随着构建流程的推进会触发各个钩子回调,并传入上下文参数,每个钩子传递的上下文参数不同,但主要包含如下几种类型
complation
:单次构建管理器compiler
:全局构建管理器module
:资源模块chunk
:模块封装容器stats
:构建过程收集到的统计信息
- webpack 为上述的上下文参数提供了多种 Side Effect 的交互接口,插件通过调用上下文接口、修改上下文状态等方式「篡改」构建逻辑,从而将扩展代码「勾入」到 Webpack 构建流程中,比如
compilation.assets
:获取或修改产物列表compilation.errors/wranings
:输出错误/警告信息,处理异常信息compilation.warnings
:输出警告信息
// 编写插件的重点:
// 1. Hook 调用时机
// 2. Hook 回调传递的上下文参数
class MyWebpackPlugin {
constructor(opts) {}
apply(compiler) {
compiler.hooks.emit.tapAsync(this.name, (compilation, cb) => {});
}
}
- 日志处理
- 多数情况下使用
compilation.errors/warnings
,柔和地抛出错误信息 - 特殊场景,需要提前结束构建时,则直接
throw new Error
抛出异常 - 拿捏不准的时候,使用
callback(err)
透传错误信息,交由上游调用者自行判断处理措施
- 多数情况下使用
- 上报统计信息
- 使用
ProgressPlugin
插件(Webpack 内置用于展示构建进度的插件)的reportProgress
接口上报执行进度 - 使用
stats
接口汇总插件运行的统计数据
- 使用
- 校验配置参数:借助
schema-utils
- 参数:根据用户配置、命令行参数和 webpack 底层默认配置,合并出完整的参数;
- Compiler:用上面构建出的参数,创建 Compiler 对象(全局构建管理器)
- Plugins:遍历 plugins 数组,执行插件的 apply 方法;根据 webpack 配置动态注入其它插件(比如根据 mode 值注入一些对 development 或 production 模式友好的插件)
- Complilation:执行 compiler.run(), 生成 Complilation 对象(单次构建管理器)Depen
- Dependence:确定入口,根据 entry 配置,执行 compilation.addEntry(), 将入口模块转换成 Dependence 对象。
- 调用相关 loader,将入口文件的内容转换成标准的 JS;
- 再调用 JS 解析器 Acorn,将 JS 转换成 AST,并从 AST 中获取该模块的依赖;
- 遍历依赖,重复上面两个步骤的处理,获取所有模块的标准 JS 内容和依赖,最终生成 ModuleGraph 对象。
- Chunk, ChunkGraph:遍历 ModuleGraph 对象,根据模块之间的关系,将模块封装进若干 Chunk 中,并根据 Chunk 之间的关系创建 ChunkGraph 对象;
- 优化:对每一个 Chunk 执行一些优化操作,比如 tree-shaking,splitChunks,压缩等;
- Asset:将最终处理好的内容,封装成 Asset,写入 output 配置的出口文件中。