webpack✅
有用过 webpack 或其他打包工具么,它们有什么特点解决什么问题?
答案
-
打包工具解决的核心问题
问题类型 具体解决方案 模块化问题 - 支持多种模块规范(CommonJS/ESM/AMD)- 处理模块依赖- 解决模块加载顺序 兼容性问题 - 语法转换(ES6+ → ES5)- Polyfill 注入- 浏览器适配 性能优化 - 代码压缩- Tree Shaking- 代码分割- 按需加载 开发效率 - 热更新- 开发服务器- 构建速度优化- 开发体验改善 资源处理 - 各类资源模块化- 资源转换和优化- 静态资源导入 -
主流打包工具对比
打包工具 主要特点 适用场景 Webpack - 生态最完善- 配置灵活- 功能强大- 支持多种模块规范 - 大型复杂项目- 需要深度定制- 老项目维护 Vite - 开发启动快- 热更新快- 配置简单- 基于ESM - 中小型项目- 新项目- 追求开发体验 Rollup - 输出更纯净- Tree-shaking 好- 插件机制简单 - 打包类库- SDK开发- 工具库 Rspack - Rust实现,速度极快- Webpack兼容API- 内置开箱即用优化- 极少的配置 - 对构建性能要求高- 大型项目迁移优化- 希望兼容Webpack生态 ESBuild - Go语言编写,超高性能- 极简设计,专注速度- 最小化配置需求- 支持JS/TS/CSS - 构建工具底层依赖- 简单项目打包- 对速度极致要求场景 -
选型建议
场景 推荐工具 原因 大型应用 Webpack 生态完善、配置灵活、功能全面 中小项目 Vite 启动快、配置简单、开发体验好 类库开发 Rollup 输出清晰、体积小、Tree-shaking 优秀
延伸阅读
- 构建系统 讲解了构建系统的含义和前端 bundler 的差异
- bundler 设计取舍 讲解了 rspack 设计理念,也概述个 bundler 的特点
- webapcke slide webpack 作者的相关分享
- awsome webpack webpack 官网相关资料汇总
webpack 的主要配置项有哪些
答案
- entry: 配置模块的入口路径
- 注意除了常规的配置文件路径,entry 也支持数组或者函数,一般数组用来实现 MPA 或者库项目打包为不同格式,函数则提供更加定制化的能力
- output 配置 webpack 的输出路径,和输出资源名称
- Loaders 扩展webpack 资源处理能力,支持类似 jsx、ts、css 图片等其他资源处理
- Plugins plugin 本质也是 loader ,只是职责上 loader 是处理资源,plugin 则用来处理其他类型的打包任务,比如打包优化、资源管理、注入环境变量等
- Mode 控制 webpack 的输出方式,在 production 下 webpack 会做很多内置优化
一个典型的示例配置如下
webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// 设置模式,可选值: development, production, none
mode: 'production',
// 入口文件配置
entry: {
main: './src/index.js',
vendor: ['react', 'react-dom']
},
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: '[name].[contenthash].js', // 输出文件名
chunkFilename: '[name].[contenthash].chunk.js', // 非入口chunk的名称
clean: true // 构建前清空输出目录
},
// 模块解析配置
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'], // 自动解析的扩展名
alias: {
'@': path.resolve(__dirname, 'src') // 创建别名
}
},
// 模块处理规则
module: {
rules: [
// JavaScript/TypeScript 处理
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
cacheDirectory: true // 启用缓存
}
}
},
// CSS 处理
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 提取CSS到单独文件
'css-loader', // 解析CSS
'postcss-loader' // 处理CSS前缀等
]
},
// 图片处理
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset', // webpack5的新资源模块类型
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 小于10kb的图片转为base64
}
},
generator: {
filename: 'images/[hash][ext][query]' // 输出到images文件夹
}
},
// 字体处理
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]' // 输出到fonts文件夹
}
}
]
},
// 插件配置
plugins: [
// 生成HTML文件
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
minify: {
removeComments: true,
collapseWhitespace: true
}
}),
// 提取CSS
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css'
})
],
// 优化配置
optimization: {
minimize: true, // 启用压缩
minimizer: [
// 压缩JS
new TerserPlugin({
extractComments: false, // 不提取注释
terserOptions: {
compress: {
drop_console: true // 移除console
}
}
}),
// 压缩CSS
new CssMinimizerPlugin()
],
// 代码分割配置
splitChunks: {
chunks: 'all', // 对所有模块进行分割
cacheGroups: {
// 第三方库分组
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// 公共模块分组
common: {
name: 'common',
minChunks: 2, // 至少被引用两次才会被提取
chunks: 'all',
priority: 5
}
}
}
},
// 开发服务器配置
devServer: {
static: {
directory: path.join(__dirname, 'public') // 静态资源目录
},
compress: true, // 启用gzip压缩
port: 8080, // 端口号
hot: true, // 热更新
historyApiFallback: true, // 支持SPA路由
open: true // 自动打开浏览器
},
// source map 配置
devtool: 'source-map', // 生产环境可改为 'hidden-source-map'
// 缓存配置
cache: {
type: 'filesystem' // 使用文件系统缓存
},
// 性能提示配置
performance: {
hints: 'warning', // 警告,而不是错误
maxAssetSize: 512000, // 单个资源最大大小 (500kb)
maxEntrypointSize: 512000 // 入口资源最大大小 (500kb)
}
}
module、chunk 、output 的区别
答案
- module webpack 将项目中每一个文件看做一个 module,webpack 支持多种 module,比如 ESM、CommonJS、AMD、Assets(各种图片、css 等资源)、WebAssembly
- chunk module 对应的中间产物,用来构建输出,分为两种
- initial entry 配置的入口文件对应的 chunk ,会把相关依赖打包为一个输出
- non-initial 异步加载等模块或者配置了 SplitChunksPlugin,后续会单独打包作为输出
- output 基于 chunk 生成的输出
- initial chunk 对应 output.filename 的输出
- non-initial chunk 对应 output.chunkFilename
此外 output 也支持一些 占位策略
参考 github 对 chunk 概念的讨论 chunk 也可按照功能分为
- Entry Chunk 包含启动代码的 Chunk
- Initial Chunk 非 Entry Chunk, 同步加载内容
- Normal Chunk 异步加载的代码,Normal Chunk。
延伸阅读
- under the hood 官方文档对上述概念的说明
- modules 官方文档对 module 的定义
- bundle chunk github issue 讨论了 chunk 的概念
loader 和 plugin 是什么,有什么区别, 用过哪些 loader 和 plugin
答案
对比项 | Loader | Plugin |
---|---|---|
作用 | 转换单个文件,如 JS、CSS、图片等 | 扩展 Webpack 编译生命周期,修改构建流程 |
执行时机 | 在模块解析阶段(ModuleGraph 生成时) | 在整个 Webpack 生命周期中运行 |
影响范围 | 影响单个文件 | 影响整个构建过程 |
使用方式 | module.rules | plugins |
示例 | babel-loader, css-loader | MiniCssExtractPlugin, HtmlWebpackPlugin |
常用的 loader
-
转译器
babel-loader
加载 ES2015+ 代码并使用 Babel 转译为 ES5ts-loader
像 JavaScript 一样加载 TypeScript 2.0+
-
模板引擎
html-loader
将 HTML 导出为字符串,要求引用静态资源pug-loader
加载 Pug 和 Jade 模板并返回一个函数markdown-loader
将 Markdown 编译为 HTMLreact-markdown-loader
使用 markdown-parse 解析器将 Markdown 编译为 React 组件
-
样式处理
style-loader
将模块的导出作为样式添加到 DOMcss-loader
加载 CSS 文件并解析导入,返回 CSS 代码less-loader
加载并编译 LESS 文件sass-loader
加载并编译 SASS/SCSS 文件postcss-loader
使用 PostCSS 加载并转化 CSS/SSS 文件stylus-loader
加载并编译 Stylus 文件
-
框架支持
vue-loader
加载并编译 Vue 组件angular2-template-loader
加载并编译 Angular 组件
注意对于 webpack4 之前常用的 raw-loader、url-loader、file-loader 等插件,在 webpack 均内置通过 asset/resource
等来配置 type 替换, 详见 Asset Modules
常用的 plugin
Webpack 拥有丰富的插件接口。Webpack 本身的大部分功能都使用了这个插件接口,使其 灵活可扩展。
名称 | 描述 |
---|---|
CommonsChunkPlugin | 提取多个代码块共享的公共模块 |
CompressionWebpackPlugin | 预压缩资源文件,以便通过 Content-Encoding 提供压缩版本 |
ContextReplacementPlugin | 覆盖 require 表达式推断出的上下文 |
CopyWebpackPlugin | 复制单个文件或整个目录到构建目录 |
DefinePlugin | 允许在编译时配置全局常量 |
DllPlugin | 拆分代码块,以大幅提升构建速度 |
EnvironmentPlugin | DefinePlugin 的简写形式,可直接用于 process.env 变量 |
EslintWebpackPlugin | Webpack 的 ESLint 插件 |
HotModuleReplacementPlugin | 启用模块热替换(HMR) |
HtmlWebpackPlugin | 轻松创建 HTML 文件以提供打包后的资源 |
IgnorePlugin | 从打包中排除特定模块 |
LimitChunkCountPlugin | 设置代码块的最小/最大数量,以更好地控制拆分 |
MinChunkSizePlugin | 确保代码块大小不低于指定限制 |
MiniCssExtractPlugin | 为每个引入 CSS 的 JS 文件创建单独的 CSS 文件 |
NoEmitOnErrorsPlugin | 当出现编译错误时跳过生成阶段 |
NormalModuleReplacementPlugin | 替换匹配正则表达式的资源 |
NpmInstallWebpackPlugin | 在开发过程中自动安装缺失的依赖 |
ProgressPlugin | 报告编译进度 |
ProvidePlugin | 允许在代码中无需 import/require 直接使用模块 |
SourceMapDevToolPlugin | 提供更精细的源码映射控制 |
SvgChunkWebpackPlugin | 根据入口依赖项生成经过 SVGO 优化的 SVG 雪碧图 |
TerserPlugin | 使用 Terser 对 JS 进行压缩优化 |
更多第三方插件可参阅 awesome-webpack 列表。
如何写一个 loader
答案
loader 函数,接受原始文本流,输出新的文本流。用于处理打包中的各种资源,参看如下示例 实现在代码的 console 中追加原始文件路径,方便调试的功能
module.exports = function (source) {
// 获取当前文件路径(相对于项目根目录)
const resourcePath = this.resourcePath
const relativePath = resourcePath.replace(process.cwd(), '').replace(/\\/g, '/')
// 定义需要处理的 console 方法
const consoleMethods = ['log', 'info', 'warn', 'error', 'debug']
let modifiedSource = source
// 处理每个 console 方法
consoleMethods.forEach(method => {
// 使用更精确的正则表达式匹配 console.method( 或 console.method ( 的情况
// 同时处理可能的空格和换行
const regex = new RegExp(`console\\s*\\.\\s*${method}\\s*\\(`, 'g')
modifiedSource = modifiedSource.replace(regex, `console.${method}("[${relativePath}]", `)
})
// 添加调试信息
console.log(`[TagLog Loader] Processing file: ${relativePath}`)
return modifiedSource
}
核心的注入 api 如下
延伸阅读
- loader 官方文档讲解 loader 核心概念
- loader api 官方文档说明 loader 下支持的 api
如何写一个 plugin ?
答案
plguin 的本质是利用 webpack 生命周期中的各种 Hook 来控制构建流程和输出,下面插件实现了提取代码中的 TODO 注释输出到项目根目录的功能。
const fs = require('fs')
const path = require('path')
class TodoPlugin {
constructor (options = {}) {
this.todos = new Map()
// 默认支持的文件后缀
this.extensions = options.extensions || ['.js', '.jsx', '.ts', '.tsx']
}
apply (compiler) {
compiler.hooks.afterCompile.tapAsync('TodoPlugin', (compilation, callback) => {
this.todos.clear()
// 获取所有模块
const modules = compilation.modules
// 处理所有模块
modules.forEach(module => {
// 检查模块是否有源文件路径
const resource = module.resource
if (!resource) return
// 检查文件扩展名
const ext = path.extname(resource)
if (!this.extensions.includes(ext)) return
// 排除 node_modules
if (resource.includes('node_modules')) return
// 处理文件
this.processFile(resource)
})
// 生成 TODO.md 内容
let todoContent = '# TODO List\n\n'
// 按照文件路径排序,使输出更稳定
const sortedEntries = Array.from(this.todos.entries())
.sort(([pathA], [pathB]) => pathA.localeCompare(pathB))
// 生成 markdown
sortedEntries.forEach(([file, todos]) => {
const basename = path.basename(file)
const relativePath = path.relative(process.cwd(), file)
todoContent += `## [${basename}](${relativePath})\n`
// 按行号排序 TODO 项
const sortedTodos = todos.sort((a, b) => a.line - b.line)
sortedTodos.forEach(todo => {
const location = `${relativePath}#L${todo.line},${todo.column}`
todoContent += `* [ ] [${todo.text}](${location})\n`
})
todoContent += '\n'
})
// 将 TODO.md 写入项目根目录
const outputPath = path.join(
compiler.context, // 使用 compiler.context 获取项目根目录
'TODO.md'
)
fs.writeFileSync(outputPath, todoContent)
callback()
})
}
processFile (filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8')
const lines = content.split('\n')
// 使用正则表达式匹配 TODO 注释
const todoRegex = /\/\/\s*TODO:\s*(.+)/
lines.forEach((line, index) => {
const match = line.match(todoRegex)
if (match) {
const todoText = match[1].trim()
const lineNumber = index + 1
const columnNumber = line.indexOf('TODO:') + 1
if (!this.todos.has(filePath)) {
this.todos.set(filePath, [])
}
this.todos.get(filePath).push({
text: todoText,
line: lineNumber,
column: columnNumber
})
}
})
} catch (error) {
console.error(`Error processing file ${filePath}:`, error)
}
}
}
module.exports = TodoPlugin
延伸阅读
- writing a plugin 官方文档详细讲解 plugin 编写
- 编译器的 hook
- rspack hooks 由于 rspack 和 webpack 的兼容性,可以参考这个图来理解整个 hook
webpack 原理
答案
关键流程分为三个阶段
- 生成 ModuleGraph,基于传入的入口文件,递归解析所有依赖,形成依赖树。其中的核心步骤包括
- 读取入口文件内容,将其解析为 AST,读取 AST 中的依赖节点,递归解析依赖
注意这里对于 js 或 ts 会采用 Acorn 解析依赖,其他资源会基于 loader 处理
- 基于配置的 loader 对匹配的文件进行转换,如将 ES6 转换为 ES5
- 读取入口文件内容,将其解析为 AST,读取 AST 中的依赖节点,递归解析依赖
- 生成 ChunkGraph 基于解析的依赖树,生成 chunk,其中的核心步骤包括
- 根据入口文件和依赖关系,生成 chunk
- 根据配置的插件,对 chunk 进行优化,如代码压缩、拆分、按需加载等
- 输出 bundle,将 chunk 输出为 bundle 文件,其中的核心步骤包括
- 将 chunk 转换为对应宿主环境的 bundle 文件
- 输出 bundle 文件到指定目录
上面的核心数据流可以概括为 入口文件文本流 -> (生成模块依赖图) ModuleGraph -Loader 处理-> ChunkGraph -各种优化-> bundle 输出
如下是一个 mvp 版本的 webpack 实现,实现了基本的依赖解析和打包功能
Terminal
延伸阅读
- how webpack work webpack 作者对 webpack 原理讲解,涉及了更多细节
- Chunk Graph Algorithm webpak 作者对 chunk graph 的实现细节讲解
- The Contributors Guide to webpack webpack 核心贡献者对 webpack 的实现细节讲解, 有三篇文章
webpack 输出的 bundle 是如何实现加载顺序不影响运行的
// 假设 webpack 配置如下
export default {
entry: './src/main.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/'
},
// webpack config ...
runtimeChunk: 'single', // 单独打包 runtime 代码
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
vendor: { // node_modules 内容拆包到 vendor
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
},
commons: { // 源码内容拆包到 commons
test: /[\\/]src[\\/]utils\.js/,
name: 'commons',
chunks: 'all'
}
}
}
}
// 输出的打包资源为
// runtime.js, main.js, commons.js,vendor.js
// 为什么脚本的加载顺序,不影响 main.js 正确执行,比如
// 加载顺序为 main.js -> commons.js -> vendor.js -> runtime.js 任然可以正常工作
答案
runtime.js
本质就是 webpack 封装的一些启动代码,来确保打包后的内容能够在浏览器中正常运行,核心逻辑包括:
- 模拟 commonjs 的 require 函数,处理模块加载
- 判断模块依赖和加载顺序的相关函数, 比如 webpackJsonpCallback、同步、异步的 chunk 处理函数等
- 工具函数
- 非 runtime.js 构建后代码,本质就是往一个全局数组里推入 chunk 信息。以 main.js 输出的代码为例,可简化为
(self.webpackChunk = self.webpackChunk || []).push([
// 对应输出的chunkId
['main'],
// 该 chunk 对应的 module map
{
// 依赖模块代码
'./src/main.js': () => {},
'./src/vendor.js': () => {}
},
// 该 chunk 运行是函数,一般 main 会存在
() => {}
])
- 其他模块的输出同理,不论 script 标签按照什么顺序加载,本质就是往全局 webpackChunk 数组推入模块信息,最终结构如下
webpackChunk = [
[['main'], { /** main chunk 对应 module map */ }, () => { /** main 对应的启动代码 */ }], // main chunk 信息
[['vendor'], { /** vendor chunk 对应的 module map */}], // vendor chunk 信息
[['commons'], { /** commons chunke 对应的 module map */}] // common chunk 信息
]
- 在执行到
runtime.js
的逻辑时,就是一个 IIFE,核心逻辑为- 注入一系列状态和函数
- runtime.js 会尝试加载
main.js
中绑定的运行函数,如果存在依赖则将 main.js 对应的 chunk 推入一个延迟数组中 runtime.js
会改写webpackChunk.push
的方法,在其中注入钩子,再后续每次推入新 chunk 数组信息的时候,都会对延迟队列进行检查,如果延迟队列中的 chunk 依赖都加载完毕则触发执行。 对于在runtime.js
之前推入的 chunk 会做一次遍历,执行上述逻辑
- 简化后逻辑
- 详细代码
/* eslint-disable camelcase */
(() => { // webpack 启动逻辑
/* chunk 加载的逻辑 */
(() => {
// 延迟队列,用于存储需要延迟执行的模块
const deferred = []
// 延迟队列管理函数
__webpack_require__.O = (result, chunkIds, fn, priority) => {
// 核心逻辑包括
// 1. 如果执行有依赖推入延迟队列
// 2. 如果未传入 chunkIds,检查队列中模块的依赖是否已加载
// 3. 如果依赖都加载完毕,执行该 chunk 的执行函数
// 4. 返回模块的执行结果
}
})();
/* jsonp chunk loading */
(() => {
// JSONP 回调函数,用于处理异步加载的 chunk
const webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
// 这里获取的就是数组推入的模块信息
const chunkIds = data[0] // 当前加载的 chunk ID 列表
const moreModules = data[1] // 当前 chunk 中的模块定义
const runtime = data[2] // 运行时代码
// 后续核心逻辑为
// 1. 如果存在父级回调函数,调用它
// 2. 标记 chunk 为已加载,并触发延迟队列检查
// 3. 返回模块的执行结果
}
// 全局数组,用于存储所有加载的 chunk 信息
const chunkLoadingGlobal = self.webpackChunk = self.webpackChunk || []
// 处理之前已加载的 chunk
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0))
// 重写 push 方法,确保后续加载的 chunk 能立即触发回调
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
})()
/************************************************************************/
})()
/* eslint-disable camelcase */
/*
* 注意: 当前使用的是 "eval" devtool(可能是开发模式下的默认配置)。
* 这种 devtool 使用 "eval()" 调用来生成浏览器开发工具中的源文件映射。
* 如果你正在阅读输出文件,请选择其他 devtool(https://webpack.js.org/configuration/devtool/)
* 或禁用默认 devtool(设置 "devtool: false")。
* 如果需要生产环境的输出文件,请使用 "production" 模式(https://webpack.js.org/configuration/mode/)。
*/
/******/ (() => { // webpackBootstrap
/******/'use strict'
// 模块定义对象,用于存储所有模块的代码
const __webpack_modules__ = ({})
/************************************************************************/
// 模块缓存对象,用于存储已加载的模块,避免重复加载
const __webpack_module_cache__ = {}
// 模块加载函数,类似于 CommonJS 的 require
function __webpack_require__ (moduleId) {
// 检查模块是否在缓存中
const cachedModule = __webpack_module_cache__[moduleId]
if (cachedModule !== undefined) {
return cachedModule.exports // 如果已缓存,直接返回模块的导出对象
}
// 创建一个新的模块对象并放入缓存
const module = __webpack_module_cache__[moduleId] = {
// 模块的导出对象
exports: {}
}
// 执行模块定义函数,将模块的导出对象填充
__webpack_modules__[moduleId](module, module.exports, __webpack_require__)
// 返回模块的导出对象
return module.exports
}
// 暴露模块定义对象,方便运行时访问所有模块定义
__webpack_require__.m = __webpack_modules__;
/************************************************************************/
/* webpack/runtime/chunk loaded */
(() => {
// 延迟队列,用于存储需要延迟执行的模块
const deferred = []
// 延迟队列管理函数
__webpack_require__.O = (result, chunkIds, fn, priority) => {
if (chunkIds) {
// 如果传入 chunkIds,表示需要将模块推入延迟队列
priority = priority || 0 // 如果未指定优先级,默认为 0
// 按优先级从高到低插入队列
for (var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1]
deferred[i] = [chunkIds, fn, priority] // 将模块信息存入队列
return
}
// 如果未传入 chunkIds,检查队列中模块的依赖是否已加载
let notFulfilled = Infinity
for (var i = 0; i < deferred.length; i++) {
var chunkIds = deferred[i][0] // 模块的依赖列表
var fn = deferred[i][1] // 模块的执行函数
var priority = deferred[i][2] // 模块的优先级
let fulfilled = true
for (var j = 0; j < chunkIds.length; j++) {
// 检查依赖的 chunk 是否已加载
if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
chunkIds.splice(j--, 1) // 如果已加载,从依赖列表中移除
} else {
fulfilled = false // 如果未加载,标记为未满足
if (priority < notFulfilled) notFulfilled = priority
}
}
if (fulfilled) {
// 如果所有依赖已加载,执行模块并从队列中移除
deferred.splice(i--, 1)
const r = fn()
if (r !== undefined) result = r
}
}
return result // 返回模块的执行结果
}
})();
/* webpack/runtime/define property getters */
(() => {
// 定义属性工具函数,为模块的 exports 定义 getter 函数
__webpack_require__.d = (exports, definition) => {
for (const key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })
}
}
}
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
// 简化版的 hasOwnProperty,用于检查对象是否拥有某个属性
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/make namespace object */
(() => {
// 为模块的 exports 添加 __esModule 标志,用于区分 ES 模块和 CommonJS 模块
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
}
Object.defineProperty(exports, '__esModule', { value: true })
}
})();
/* webpack/runtime/jsonp chunk loading */
(() => {
// 用于存储已加载和正在加载的 chunk 状态
// undefined = chunk 未加载, null = chunk 已预加载/预取, [resolve, reject, Promise] = chunk 正在加载, 0 = chunk 已加载
const installedChunks = {
runtime: 0 // runtime chunk 已加载
}
// 检查 chunk 是否已加载
__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0)
// JSONP 回调函数,用于处理异步加载的 chunk
const webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
const chunkIds = data[0] // 当前加载的 chunk ID 列表
const moreModules = data[1] // 当前 chunk 中的模块定义
const runtime = data[2] // 运行时代码
let moduleId; let chunkId; let i = 0
if (chunkIds.some((id) => (installedChunks[id] !== 0))) {
// 注册模块到 __webpack_require__.m 中
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId]
}
}
// 如果存在运行时代码,执行它
if (runtime) var result = runtime(__webpack_require__)
}
// 如果存在父级回调函数,调用它
if (parentChunkLoadingFunction) parentChunkLoadingFunction(data)
// 标记 chunk 为已加载,并触发延迟队列检查
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i]
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]()
}
installedChunks[chunkId] = 0
}
return __webpack_require__.O(result)
}
// 全局数组,用于存储所有加载的 chunk 信息
const chunkLoadingGlobal = self.webpackChunk = self.webpackChunk || []
// 处理之前已加载的 chunk
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0))
// 重写 push 方法,确保后续加载的 chunk 能立即触发回调
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
})()
/************************************************************************/
})()
了解 tapable 么?
答案
tapable 是一个工具包。 webpack 基于它实现了底层的插件体系和事件流控制功能。 tapable 核心功能如下
- 插件能力,通过创建 Hook, 插件绑定 Hook 事件。
- 事件流控制能力,通过 SyncHook, SyncWaterfallHook, AsyncParallelHook, 等一系列 Hook 提供对同步或异步流程串行,并行,瀑布流等控制
典型的示例
const { SyncHook, AsyncSeriesHook } = require('tapable')
// 一个简化版的文本流处理工具,模拟 Webpack 插件架构
class TextProcessor {
constructor () {
// 定义钩子
this.hooks = {
// 同步钩子:文本处理开始时触发
beforeProcess: new SyncHook(['content']),
// 异步钩子:文本处理完成后触发
afterProcess: new AsyncSeriesHook(['content'])
}
}
// 注册插件
apply (plugin) {
plugin.apply(this)
}
// 处理文本
async process (content) {
console.log('Processing started...')
this.hooks.beforeProcess.call(content) // 触发 beforeProcess 钩子
// 模拟文本处理逻辑
const processedContent = content.toUpperCase()
console.log('Processing completed...')
await this.hooks.afterProcess.promise(processedContent) // 触发 afterProcess 钩子
return processedContent
}
}
// 插件:日志插件
class LogPlugin {
apply (processor) {
processor.hooks.beforeProcess.tap('LogPlugin', (content) => {
console.log(`[LogPlugin] Before processing: ${content}`)
})
processor.hooks.afterProcess.tapPromise('LogPlugin', async (content) => {
console.log(`[LogPlugin] After processing: ${content}`)
return new Promise((resolve) => setTimeout(resolve, 500)) // 模拟异步操作
})
}
}
// 插件:统计插件
class StatsPlugin {
apply (processor) {
processor.hooks.afterProcess.tapPromise('StatsPlugin', async (content) => {
console.log(`[StatsPlugin] Processed content length: ${content.length}`)
return new Promise((resolve) => setTimeout(resolve, 300)) // 模拟异步操作
})
}
}
// 使用文本处理工具
(async () => {
const processor = new TextProcessor()
// 注册插件
processor.apply(new LogPlugin())
processor.apply(new StatsPlugin())
// 处理文本
const inputText = 'Hello, Tapable!'
const result = await processor.process(inputText)
console.log(`Final processed content: ${result}`)
})()
webpack 优化
答案
Webpack 优化可分为两大类:开发构建优化(提升构建速度)和生产构建优化(减小产物体积、提升运行性能)。
-
开发构建优化
-
缩小模块搜索范围
include/exclude
缩小 loader 处理范围(排除 node_modules)resolve.extensions
设置合理的后缀查找顺序resolve.alias
配置路径别名减少查找步骤resolve.modules
指定第三方模块目录位置
-
利用缓存提升二次构建速度
babel-loader
配置cacheDirectory: true
cache-loader
缓存其他 loader 结果- Webpack 5 的
cache: { type: 'filesystem' }
持久化缓存
-
并行与预编译
thread-loader
开启多进程处理耗时任务- 使用
DllPlugin
预编译稳定依赖
-
开发体验优化
- 合理配置
devServer
- 启用
HMR
热模块替换
- 合理配置
-
-
生产构建优化
-
文件压缩与优化
- JS: 使用
TerserPlugin
压缩 - CSS: 使用
CssMinimizerPlugin
压缩,搭配MiniCssExtractPlugin
提取独立CSS - HTML: 配置
HtmlWebpackPlugin
的minify
选项 - 图片: 使用
image-webpack-loader
压缩图像资源 - 启用
Gzip/Brotli
压缩:CompressionWebpackPlugin
- JS: 使用
-
代码分割与按需加载
- 配置
splitChunks
抽取公共代码 - 使用
import()
实现动态导入,配合webpackChunkName
合理命名 - 基于
prefetch/preload
预加载关键资源
- 配置
-
打包体积优化
- 启用
Tree Shaking
(production 模式自动启用) - CSS Tree Shaking:
PurgecssWebpackPlugin
- 使用
externals
排除不需打包的库 ModuleConcatenationPlugin
启用作用域提升
- 启用
-
性能分析与监控
webpack-bundle-analyzer
分析包体积speed-measure-webpack-plugin
检测构建速度瓶颈
-
-
其他策略
-
差异化构建
- 现代浏览器使用ES6+模块,旧浏览器使用ES5兼容版本
- 使用
browserslist
精准控制目标环境
-
模块联邦
- 适用于微前端场景的依赖共享与动态加载
-
延伸阅读
- webpack 5 优化指南
- 前端优化 webpack 作者对前端优化的总结
说下异步加载
答案
-
解决什么问题 动态加载(按需加载)主要解决大型应用初始加载性能问题。通过将代码分割成小块并在需要时才加载,可以减少初始加载时间、提高首屏渲染速度,并优化资源利用率。
-
如何使用
- 采用
import()
语法进行动态加载, 魔法注释webpackChunkName
可控制生成的 chunk 名称,还支持webpackPrefetch
、webpackPreload
等优化导入行为。 - 使用
React.lazy
和Suspense
进行组件懒加载,结合import()
实现按需加载。 - Vue 3.x 中使用
defineAsyncComponent
实现异步组件加载。
// 使用 ES6 动态导入(推荐方式)
import(/* webpackChunkName: "my-chunk" */ './module')
.then(module => {
// 使用模块
})
// 与 React.lazy 结合使用
const MyComponent = React.lazy(() => import('./MyComponent')) - 采用
-
原理
webpack 的动态加载核心原理包括:
- 构建时拆分:遇到
import()
语句时,webpack 将其识别为分割点 - 分包生成:分离出独立的 chunk 文件而非并入主 bundle
- 运行时加载:生成 JSONP 调用代码,在需要时创建
<script>
标签请求相应 chunk - 依赖解析:chunk 加载完成后自动解析并执行,提供给调用方使用
webpack 5 改进了缓存算法并优化了动态加载性能,提供更高效的代码分割实现。
tree-shaking
答案
-
解决问题
- 消除未使用代码("死代码")
- 减小打包体积
- 提高应用性能和加载速度
-
使用方法
- 使用ES6模块语法(import/export)
- webpack配置中设置
mode: "production"
- 若项目有副作用,可通过
sideEffects
配置标记
-
原理
- 基于ES6模块静态特性的静态分析
- webpack解析代码构建依赖图谱
- 标记未使用的模块和导出
- 在打包过程中移除未使用代码
-
注意事项
- 主要对ES6模块有效(webpack 5已支持部分CommonJS)
- 动态导入(
import()
)会影响分析 - 副作用代码会被保留(可用
/*#__PURE__*/
标记纯函数) - 条件引用、函数式编程方式可能导致分析失效
- 确保package.json中合理配置
sideEffects
sideEffects 属性的作用是啥
答案
-
sideEffects 解决什么问题
sideEffects 解决的是 tree shaking (摇树优化)中的副作用识别问题。它帮助 webpack 确定哪些模块在导入但未使用其导出内容时可以被安全移除,从而减小打包体积,提高应用性能。
-
sideEffects 如何使用
在 package.json 中配置:
{
"sideEffects": false // 所有文件都无副作用
// 或
"sideEffects": ["*.css", "*.scss"] // 指定有副作用的文件
} -
原理和注意事项
- 原理: webpack 检查模块的 sideEffects 标记,确定哪些导入但未使用的代码可以被安全移除。
- 注意事项:
- 错误标记会导致必要代码被删除
- CSS 文件、全局样式通常需标记为有副作用
- polyfills 和全局修改代码必须标记为有副作用
- 可结合
/*#__PURE__*/
注释标记纯函数调用
webpack externals 是如何加载外部依赖的
答案
-
解决什么问题
- 减小打包体积:将指定依赖排除在最终bundle之外
- 避免重复打包:利用已通过其他方式加载的库(如CDN)
- 优化加载性能:减少初始加载时间和浏览器解析负担
-
如何使用
// webpack.config.js
module.exports = {
externals: {
jquery: 'jQuery', // 模块名: 全局变量名
lodash: '_',
react: 'React'
}
}然后在HTML中引入外部资源:
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
-
原理
- 构建时:webpack遇到被标记为external的模块引用时不会将其打包
- 运行时:基于配置的映射关系确定如何获取依赖
- 浏览器环境:查找指定的全局变量(window.jQuery)
- Node环境:通过require加载外部模块
- 不同模块系统(UMD/CommonJS/AMD):采用对应的解析策略
externals配置支持多种形式,可根据运行环境和模块类型灵活处理依赖加载方式。
DllPlugin
答案
-
解决问题
- 加快构建速度:预编译稳定依赖,避免每次构建重复分析第三方库
- 提高开发效率:减少开发环境构建时间
-
使用方法
- 创建单独的webpack配置文件(webpack.dll.config.js)打包第三方库
- 使用DllPlugin生成manifest.json映射文件
- 在主配置中使用DllReferencePlugin引用预编译的库
// webpack.dll.config.js
new webpack.DllPlugin({
name: '[name]',
path: path.join(__dirname, 'dist', '[name].manifest.json')
})
// webpack.config.js
new webpack.DllReferencePlugin({
manifest: require('./dist/vendor.manifest.json')
}) -
原理
- 将稳定依赖单独打包为动态链接库
- 生成manifest清单记录模块映射关系
- 主构建过程中直接引用预编译模块而非重新分析打包
-
局限与注意事项
- 需要额外的构建步骤和配置
- 依赖变更时需要重新构建DLL
- Webpack 5内置缓存已能解决类似问题,使用场景减少
webpack5 Module Federation
答案
-
解决什么问题
- 实现微前端架构中多个独立应用间代码共享
- 避免重复打包相同依赖
- 支持独立开发、部署和运行的子应用间动态模块共享
-
如何使用
// 暴露模块的应用
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
}
})
// 消费模块的应用
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js'
}
})
-
原理与适用范围
- 基于运行时容器机制,动态加载远程模块
- 在构建时创建模块映射,运行时按需加载
- 适用于大型前端应用拆分、多团队协作开发
-
注意事项
- 需处理共享依赖的版本兼容性
- 可与Single-SPA等微前端框架结合使用
- 首次加载可能增加网络请求,但提升整体应用性能
延伸阅读
- module federation webpack 作者,模块联邦分享
webpack热更新原理是什么?
答案
- 解决什么问题 Hot Module Replacement (HMR) 解决了开发过程中完全刷新页面的问题。它允许在不刷新整个页面的情况下,只更新变更的模块,同时保留应用状态(如表单输入、滚动位置等),从而提高开发效率和体验。
- 如何使用 可通过以下两种方式开启:
- 在 webpack 配置中添加
new webpack.HotModuleReplacementPlugin()
- 使用 CLI 参数
webpack-dev-server --hot
- 在 webpack 配置中添加
- 热更新的核心原理
- 文件监听与编译: webpack-dev-server 使用 webpack 的 watch 模式监听文件变化,当文件变化时触发重新编译
- 服务端推送: 编译完成后,webpack-dev-server 通过 WebSocket 向客户端发送"hash"和"ok"消息
- 客户端请求更新:
- 客户端接收到消息后,通过 AJAX 请求获取热更新清单 (
.hot-update.json
) - 根据清单,通过 JSONP 请求获取更新的模块代码 (
.hot-update.js
)
- 客户端接收到消息后,通过 AJAX 请求获取热更新清单 (
- 框架对热更新的处理
- react fast refresh
- vue vue loader hot reload
webpack-dev-server 的功能
答案
webpack-dev-server 解决项目本地开发调试问题,常用核心功能如下
-
热模块替换(Hot Module Replacement) 缩写 HMR:webpack-dev-server 可以监听源文件的变化,当文件发生改动时,它会自动重新编译和打包,保证开发过程中始终使用最新的代码。
-
代理和反向代理:webpack-dev-server 可以配置代理,用于解决前端开发中跨域请求的问题。和微前端等路由映射问题。
source map 配置和原理
答案
延伸阅读
- source map 作用