- Published on
webpack相关总结
- Authors
- Name
- noodles
- 每个人的花期不同,不必在乎别人比你提前拥有
webpack基础
- 模块打包 将不同模块的文件整合在一起,保证引用顺序正确
- 编译兼容 通过loader可以实现polyfill
- 能力扩展 通过plugin扩展模块打包和编译兼容的能力
webpack打包流程
初始化阶段
- 读取合并配置参数
- 创建Compiler对象/加载插件,准备开始编译
编译阶段
- 确定入口 根据entry配置找到入口文件,构建依赖图谱
- 模块路径解析 解析模块路径
- 构建模块 通过loader加载文件解析模块内容并且收集模块依赖
生成阶段
- 确定输出内容 根据依赖图提取模块- 根据entry 和 optimization配置决定分组 分割策略
- 优化 代码分割 压缩混淆等等
- 生成资源 生成最终的chunk source map等
- 构建资源映射表 包含所有资源的路径清单 用于异步加载
Loader
将各种类型的文件转换成 Webpack 可以处理的模块
Loader的执行顺序
Loader是通过数组进行配置的,loader-runner会从配置的末尾依次执行对应loader的处理逻辑(compose) [a,b,c]


Loader开发
- this.callback 一个可以同步或者异步调用的可以返回多个结果的函数
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
- this.async 告知loader-runner这个loader将会异步回调返回this.callback
同步loader
// content
module.exports = function(content, map, meta) {
return someDealFunc(content) // 直接返回同步执行逻辑 同步loader会阻塞webpack编译
}
module.exports = function(content, map, meta) {
// 通过callback触发
this.callback(null, someDealFunc(content), map, meta)
// 需要这个return 显示的返回undefined
return
}
异步loader
module.exports = function(content, map, meta) {
// 告知loader-runner是一个异步loader 异步loader不会阻塞webpack编译
const callback = this.async();
someAsyncOperation(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
Plugin
基于webpack构建的hooks来增强构建能力
开发plugin
- 插件必须是一个函数或者一个包含apply方法的对象
- 传递给插件的compiler和compilation对象是同一个引用,修改对应的对象会对后面的插件有影响
- 异步事件需要插件调用回调函数通知webpack进入下一个流程(同步钩子会阻塞编译,异步钩子不会阻塞编译)
// 同步钩子
class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}
// 异步钩子
class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.run.tapAsync('MyPlugin', (compilation, callback) => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// 这里调用这个callback 表示这个异步操作结束了
callback()
})
compiler.hooks.run.tapPromise('MyPlugin', (compilation) => {
// compilation: 当前打包构建流程的上下文
return new Promise((resolve, reject) => {
resolve(11)
})
})
}
}
webpack性能优化
优化构建速度
缓存机制:利用 cache 选项或 cache-loader 插件来缓存构建过程中生成的中间文件,加快后续构建速度。 并行编译:使用 thread-loader 或者 Webpack 自身的多线程能力加速资源处理。
文件查找优化: resolve.modules(指定模块解析路径)/resolve.alias(别名)/resolve.extensions(扩展名尝试)/noParse(不解析特定的文件)
打包体积性能优化
Tree Shaking: 去除无用的代码
按需加载Polyfills: 按需加载polyfills
三方库: 单独拆分或者CDN引入
代码分割: optimization.splitChunks
环境变量: definePlugin
webpack相关概念/配置项
配置项
配置项 | 说明 |
---|---|
output.publicPath | 用户配置打包结果资源引用的位置 |
output.path | 影响生成文件存放的目录和引用 |
devServer.publicPath | 开发模式下的资源访问路径配置,默认 '/' |
output.filename | 输出bundle的文件名 |
output.chunkFilename | 配置非入口 chunk 的文件名,主要有异步加载模块/代码分割模块 |
相关概念
配置项 | 说明 |
---|---|
module | 需要编译处理的每个文件,模块中包含代码、依赖关系等 |
chunk | 由多个模块组成的代码块,比如Entry chunk,Split chunk拆分出的代码等 |
bundle | 最终生成的输出文件 |
sourceMap | 浏览器会通过sourceURL获取映射文件,通过解析器解析后实现源码和混淆代码之间的映射.通过会做域名的限制来防止外网访问 |
hash | 每次构建都会生成一个hash, 只要项目文件有修改整个项目的hash值就会有变化 |
contenthash | 跟单个文件内容相关,内容改变就会改变hash,通常在抽取单独的css文件的时候使用contenthash |
chunkhash | 跟生成的chunk有关,不同的chunk文件会有不同的hash,js文件会使用chunkhash |
webpack热更新 | 在浏览器和与Webpack Dev Server之间维护了一个Websocket,在监听模式下,资源变化Dev Server会向浏览器推送更新(包括构建的hash值),浏览器完成更新资源的替换 |
compiler | webpack在启动后会创建一个单例的Compiler对象,它负责存储webpack所有配置信息、插件以及声明周期钩子函数 |
compilation | compilation对象是每一次构建的上下文对象,包含当次构建的所有信息 |
打包工具对比
大型项目和企业级应用:选择 Webpack,因为它的高度可配置性和强大的插件生态系统可以满足复杂的需求。
快速原型开发和中小型项目:选择 Parcel,因为它的零配置和自动优化特性可以显著提高开发效率.
库开发和中小型项目:选择 Rollup,因为它的简洁配置和优秀的 Tree Shaking 支持可以有效减小打包体积。
特性 | Webpack | Parcel | Rollup |
---|---|---|---|
配置复杂度 | 高 | 低 | 中等 |
性能 | 中等 | 高 | 高 |
适用场景 | 大型项目、企业级应用 | 快速原型开发、中小型项目 | 库开发、中小型项目 |
模块化支持 | 支持多种模块格式 | 支持多种模块格式 | 支持多种模块格式 |
插件系统 | 丰富 | 丰富 | 丰富 |
HMR | 支持 | 支持 | 支持 |
代码分割 | 支持 | 支持 | 支持 |
Tree Shaking | 支持 | 支持 | 优秀 |
多语言支持 | 支持 | 支持 | 支持 |
webpack tree shaking的原理
Tree Shaking指在打包过程中通过静态分析的方式识别和删除没有使用过的代码,以减少最终文件的打包体积
- 识别未使用的代码 webpack会遍历整个模块依赖图,标记模块的导出和引用,并将这些信息保存到内存中
- 标记未使用的代码 通过静态分析的方式确定哪些代码没有被引用,对于没有使用的代码webpack会标记为'未使用'
- 删除未使用的代码 在打包阶段 webpack会根据标记阶段的结果,将标记为'未使用'的代码从打包的结果中删除
webpack hooks整理
初始化阶段
钩子 | 触发时机 | 类型 |
---|---|---|
environment | 初始化环境,插件初始化完触发 | SyncHook |
afterEnvironment | 在environment后触发,相关配置设置完成 | SyncHook |
entryOption | 开始处理配置文件的entry配置之后触发 | SyncBailHook |
afterPlugins | 添加完插件之后触发 | SyncHook |
afterResolvers | resolver设置完成之后触发 | SyncHook |
beforeRun | 在执行compiler的run方法之前 | AsyncSeriesHook |
编译阶段
钩子 | 触发时机 | 常见用途 | 类型 |
---|---|---|---|
run | 在开始编译之后触发 | AsyncSeriesHook | |
beforeCompile | 编译参数创建后 | AsyncSeriesHook | |
compile | 准备编译资源 | SyncHook | |
compilation | compilation创建之后执行 | SyncHook | |
make | compilation结束之前执行 | AsyncParallelHook | |
afterCompile | compilation结束和封印之后执行 | AsyncSeriesHook |
生成阶段
钩子 | 触发时机 | 常见用途 | 类型 |
---|---|---|---|
shouldEmit | 在输出 asset 之前调用 | SyncBailHook | |
emit | 输出 asset 到 output 目录之前执行 | AsyncSeriesHook | |
afterEmit | 输出 asset 到 output 目录之后执行 | AsyncSeriesHook | |
done | 在 compilation 完成时执行 | AsyncSeriesHook |
监听钩子
钩子 | 触发时机 | 常见用途 | 类型 |
---|---|---|---|
watchRun | 监听模式下,一次编译开始 | 处理监听编译 | AsyncSeriesHook |
watchClose | 监听模式停止 | 处理监听停止 | SyncHook |
webpackBootstrap 启动文件的格式
// 立即执行函数,创建独立的作用域
/******/ (() => { // webpackBootstrap
// 定义所有的模块
/******/ var __webpack_modules__ = ({
/******/ "./src/moduleA.js":
/******/ ((__unused_webpack_module, exports) => {
/******/ exports.foo = function() {
/******/ return 'Hello from moduleA';
/******/ };
/******/ })
/******/ });
/******/ // 模块缓存
/******/ var __webpack_module_cache__ = {};
// 指定模块加载逻辑
/******/ function __webpack_require__(moduleId) {
/******/ // 检查模块是否在缓存中
/******/ if(__webpack_module_cache__[moduleId]) {
/******/ return __webpack_module_cache__[moduleId].exports;
/******/ }
/******/ // 如果不在缓存中,创建新的模块并放入缓存
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ exports: {}
/******/ };
/******/ // 执行模块函数 传入预设的参数
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ // 返回模块的导出内容
/******/ return module.exports;
/******/ }
// 启动入口模块
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/ // ...
/******/ })()
webpack模块联邦
模块联邦用于解决模块的共享和加载的问题。它允许你在不同的webpack构建之间共享JavaScript模块,相比npm包管理的方式它实现更加灵活的代码共享方式,可以实现动态加载并减少的依赖管理等问题
// 主应用
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: "mainApp", // 主应用的名字
remotes: { // 远程模块配置 名称入口文件
remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js",
},
// 共享模块
shared: ["lodash"],
}),
],
};
// 远程应用
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: "remoteApp", // 远程应用的名字
filename: "remoteEntry.js", // 远程应用的入口文件
exposes: { // 指定要共享出去的模块
"./Button": "./src/Button",
},
// 共享模块
shared: ["lodash"],
}),
],
};
// 主应用中的代码 动态加载远程模块
const remoteApp = import("remoteApp/Button");
remoteApp.then((Button) => {
// 使用远程模块
const button = new Button();
document.body.appendChild(button);
});
webpack模块联邦的优势
- 动态加载模块 模块联邦可以在运行时加载模块,从而实现更加灵活的模块加载
- 模块共享 不需要通过npm管理依赖项,降级了项目依赖关系的复杂性同时提高的构建效率
- 独立部署和自治 不同应用可独立部署和自治
- 可细粒度控制 选择性的共享模块
相关文章
webpack源码解读
dive into webpack
当面试官问Webpack的时候他想知道什么
三十分钟掌握Webpack性能优化
吐血整理」再来一打Webpack面试题