Webpack 面试题
Webpack 是目前最主流的前端模块打包工具,以下涵盖核心概念、配置、性能优化、Loader/Plugin 开发等高频面试题。
1. Webpack 是什么?它的核心概念有哪些?
Webpack 是一个静态模块打包工具,它从一个或多个入口文件出发,递归地构建依赖图(dependency graph),然后将所有模块打包成一个或多个 bundle。
// webpack.config.js 最基本配置
const path = require('path')
module.exports = {
// 1. 入口(Entry):打包的起点
entry: './src/index.js',
// 2. 输出(Output):打包结果的位置和文件名
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js', // 内容哈希,用于缓存
clean: true // 打包前清空 dist 目录
},
// 3. 加载器(Loader):处理非 JS 文件(CSS、图片、TS等)
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.tsx?$/, use: 'ts-loader' },
{ test: /\.(png|jpg|gif)$/, type: 'asset/resource' }
]
},
// 4. 插件(Plugin):扩展 Webpack 的能力
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' })
],
// 5. 模式(Mode):决定内置优化策略
mode: 'production', // 'development' | 'production' | 'none'
// 6. 解析(Resolve):配置模块解析规则
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
五大核心概念对比:
| 概念 | 作用 | 示例 |
|---|---|---|
| Entry | 打包入口 | entry: './src/index.js' |
| Output | 输出配置 | output: { filename: 'bundle.js' } |
| Loader | 转换非 JS 模块 | css-loader、babel-loader |
| Plugin | 扩展功能(打包优化、资源管理) | HtmlWebpackPlugin |
| Mode | 构建模式 | development / production |
💡 面试加分点: Webpack 5 相比 4 的重大变化——内置了 Asset Modules(取代 file-loader/url-loader)、持久缓存(filesystem cache)、Module Federation(模块联邦)、不再自动 polyfill Node.js 核心模块。
2. Loader 和 Plugin 的区别?
// ========== Loader:文件转换器 ==========
// Loader 用于处理非 JS 文件,将其转换为 Webpack 能理解的模块
// 本质:一个函数,接收源文件内容,返回转换后的结果
// Loader 的执行顺序:从右到左,从下到上
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 3. 将 CSS 注入 DOM 的 <style> 标签
'css-loader', // 2. 解析 CSS 中的 @import 和 url()
'sass-loader' // 1. 将 SCSS 编译为 CSS
]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true // 开启缓存
}
}
}
]
}
}
// ========== Plugin:功能扩展器 ==========
// Plugin 可以在 Webpack 构建的各个生命周期钩子上执行操作
// 本质:一个具有 apply 方法的类
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
// 自动生成 HTML 并注入打包后的 JS/CSS
new HtmlWebpackPlugin({
template: './public/index.html',
minify: { collapseWhitespace: true, removeComments: true }
}),
// 将 CSS 提取为独立文件(生产环境)
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
}),
// 包体积分析
new BundleAnalyzerPlugin()
]
}
Loader vs Plugin 对比:
| 对比 | Loader | Plugin |
|---|---|---|
| 作用 | 文件转换(编译/转译) | 功能扩展(优化/注入/清理等) |
| 本质 | 函数 | 类(有 apply 方法) |
| 执行时机 | 在模块加载时 | 在整个编译生命周期的各个钩子 |
| 配置位置 | module.rules | plugins |
| 常见示例 | babel-loader, css-loader | HtmlWebpackPlugin, DefinePlugin |
3. 常用 Loader 有哪些?
module.exports = {
module: {
rules: [
// ===== JS/TS 处理 =====
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader', // 将 ES6+/JSX 转换为 ES5
options: {
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }],
'@babel/preset-react'
],
plugins: ['@babel/plugin-transform-runtime'],
cacheDirectory: true
}
}
},
{
test: /\.tsx?$/,
use: 'ts-loader' // 编译 TypeScript
// 或者用 babel-loader + @babel/preset-typescript
},
// ===== CSS 处理 =====
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 生产环境提取 CSS 文件
// 'style-loader', // 开发环境注入 <style> 标签
{
loader: 'css-loader', // 解析 CSS
options: { modules: true } // 开启 CSS Modules
},
'postcss-loader' // PostCSS 处理(自动前缀等)
]
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'] // Less 编译
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'] // Sass 编译
},
// ===== 资源处理(Webpack 5 内置 Asset Modules)=====
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset', // 自动选择 resource 或 inline
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于 8KB 转为 base64
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource', // 字体文件
generator: { filename: 'fonts/[name].[hash:8][ext]' }
},
// ===== Vue 单文件组件 =====
{
test: /\.vue$/,
use: 'vue-loader' // 解析 .vue 文件
},
// ===== 其他 =====
{
test: /\.md$/,
use: ['html-loader', 'markdown-loader'] // Markdown 转 HTML
}
]
}
}
💡 面试加分点: Webpack 5 的 Asset Modules 取代了
file-loader、url-loader、raw-loader。四种 type:asset/resource(输出文件)、asset/inline(转 base64)、asset(自动选择)、asset/source(导出源代码)。
4. 常用 Plugin 有哪些?
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const CompressionPlugin = require('compression-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
plugins: [
// 1. 自动生成 HTML,注入打包资源
new HtmlWebpackPlugin({
template: './public/index.html',
title: '我的应用',
minify: {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true
}
}),
// 2. 提取 CSS 为独立文件
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css'
}),
// 3. 定义环境变量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
__APP_VERSION__: JSON.stringify('1.0.0')
}),
// 4. Gzip 压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 大于 10KB 才压缩
minRatio: 0.8
}),
// 5. 复制静态资源
new CopyWebpackPlugin({
patterns: [
{ from: 'public/favicon.ico', to: 'favicon.ico' }
]
}),
// 6. 包体积分析
new BundleAnalyzerPlugin({
analyzerMode: 'static', // 生成静态 HTML 报告
openAnalyzer: false
}),
// 7. ESLint 检查
new ESLintPlugin({
extensions: ['js', 'jsx', 'ts', 'tsx'],
fix: true
}),
// 8. TypeScript 类型检查(独立进程,不阻塞编译)
new ForkTsCheckerWebpackPlugin(),
// 9. 模块热替换(开发环境)
new webpack.HotModuleReplacementPlugin(),
// 10. 进度条
new webpack.ProgressPlugin()
],
optimization: {
minimizer: [
// JS 压缩
new TerserPlugin({
parallel: true, // 多进程并行压缩
terserOptions: {
compress: { drop_console: true } // 移除 console
}
}),
// CSS 压缩
new CssMinimizerPlugin()
]
}
}
5. Webpack 的构建流程(原理)?
Webpack 构建流程(简化版):
1. 初始化(Initialization)
├── 读取并合并配置(webpack.config.js + 命令行参数 + 默认配置)
├── 创建 Compiler 对象
└── 注册所有 Plugin(调用 plugin.apply(compiler))
2. 编译(Compilation)
├── 从 Entry 开始,调用对应 Loader 转换模块内容
├── 解析模块的依赖(import/require)
├── 递归处理所有依赖模块 → 构建完整的依赖图
└── 每个文件只会被处理一次
3. 输出(Emit)
├── 根据依赖图和配置,组装 Chunk
├── 将 Chunk 转换为文件(Bundle)
└── 写入文件系统(dist 目录)
// 简化版 Webpack 核心流程伪代码
class Webpack {
constructor(config) {
this.config = config
this.modules = new Map() // 所有模块
this.chunks = [] // 代码块
this.hooks = { // 生命周期钩子
beforeRun: new SyncHook(),
run: new AsyncSeriesHook(),
compilation: new SyncHook(),
emit: new AsyncSeriesHook(),
done: new SyncHook()
}
// 注册所有插件
config.plugins.forEach(plugin => plugin.apply(this))
}
run() {
this.hooks.beforeRun.call()
// 1. 从入口开始构建
const entryModule = this.buildModule(this.config.entry)
// 2. 递归解析依赖
this.processDependencies(entryModule)
// 3. 生成 Chunk
this.chunks = this.generateChunks()
// 4. 输出文件
this.hooks.emit.callAsync(() => {
this.emitFiles()
this.hooks.done.call()
})
}
buildModule(filePath) {
// 读取文件内容
let content = fs.readFileSync(filePath, 'utf-8')
// 匹配并执行 Loader(从右到左)
const loaders = this.getMatchingLoaders(filePath)
for (const loader of loaders.reverse()) {
content = loader(content)
}
// 解析 AST,提取依赖
const ast = parse(content)
const dependencies = extractImports(ast)
return { filePath, content, dependencies }
}
}
💡 面试加分点: Webpack 使用 Tapable 库管理插件的钩子系统。整个构建过程有上百个钩子,Plugin 通过订阅这些钩子来介入构建流程。
6. Webpack 的热更新(HMR)原理?
HMR(Hot Module Replacement)工作流程:
1. 文件修改 → Webpack 重新编译修改的模块
2. Webpack Dev Server 通过 WebSocket 通知浏览器
3. 浏览器收到通知后,通过 JSONP 请求获取更新的模块
4. 替换旧模块,局部刷新(不刷新整个页面)
// webpack.config.js - 开启 HMR
module.exports = {
devServer: {
hot: true, // 开启 HMR
port: 3000,
open: true,
historyApiFallback: true // SPA 路由支持
}
}
// 手动处理 HMR(一般框架会自动处理)
if (module.hot) {
module.hot.accept('./App', () => {
// App 模块更新后的处理逻辑
const NextApp = require('./App').default
render(NextApp)
})
}
// HMR 原理示意:
// 1. 建立 WebSocket 连接
// 浏览器 ←WebSocket→ Dev Server
//
// 2. 文件变化时,服务端推送更新信息
// Server → { type: 'hash', data: 'abc123' }
// Server → { type: 'ok' }
//
// 3. 浏览器请求更新清单
// GET /abc123.hot-update.json
// → { c: { main: true }, r: [], m: [] }
//
// 4. 浏览器请求更新的模块
// GET /main.abc123.hot-update.js
// → 新的模块代码
//
// 5. 替换旧模块,执行 module.hot.accept 回调
HMR vs Live Reload 对比:
| 对比 | HMR | Live Reload |
|---|---|---|
| 更新方式 | 局部替换模块 | 刷新整个页面 |
| 状态保持 | ✅ 保持页面状态 | ❌ 状态丢失 |
| 速度 | 快(只更新变化的模块) | 慢(重新加载整个页面) |
| CSS 更新 | 无刷新即时生效 | 需要刷新 |
7. Source Map 是什么?有哪些类型?
// Source Map 是一个映射关系文件,将打包后的代码映射回源代码
// 方便在浏览器中调试压缩/编译后的代码
module.exports = {
// 开发环境推荐:有完整的源码映射,构建速度快
devtool: 'eval-cheap-module-source-map',
// 生产环境推荐:不生成 source map 或只生成 hidden
// devtool: false, // 不生成(安全)
// devtool: 'hidden-source-map', // 生成但不关联(用于错误监控)
// devtool: 'nosources-source-map', // 有行列信息但无源码
}
常见 Source Map 类型对比:
| devtool | 构建速度 | 重构建速度 | 质量 | 适用场景 |
|---|---|---|---|---|
false | 最快 | 最快 | 无 | 生产环境(安全) |
eval | 快 | 最快 | 生成代码 | 开发(速度优先) |
eval-cheap-module-source-map | 较快 | 快 | 原始源码(行) | 开发(推荐) |
eval-source-map | 慢 | 较快 | 原始源码(行+列) | 开发(质量优先) |
source-map | 最慢 | 慢 | 原始源码(行+列) | 生产(调试需要) |
hidden-source-map | 最慢 | 慢 | 原始源码 | 生产(错误监控) |
nosources-source-map | 慢 | 慢 | 无源码 | 生产(只要行列号) |
💡 面试加分点: 生产环境如果用了 source-map,一定要确保
.map文件不会被公开访问(通过 Nginx 限制或只上传到 Sentry 等监控平台)。
8. Webpack 的 Tree Shaking 原理?
// Tree Shaking:移除未使用的代码(Dead Code Elimination)
// 依赖 ES Module 的静态结构(import/export 是静态的,编译时可确定依赖关系)
// ========== 生效条件 ==========
// 1. 必须使用 ES Module(import/export),不能用 CommonJS(require/module.exports)
// 2. mode: 'production'(自动开启 Tree Shaking)
// 3. package.json 中设置 sideEffects
// utils.js
export function add(a, b) { return a + b } // ✅ 被使用,保留
export function subtract(a, b) { return a - b } // ❌ 未使用,移除
export function multiply(a, b) { return a * b } // ❌ 未使用,移除
// main.js
import { add } from './utils' // 只导入了 add
console.log(add(1, 2))
// 打包后:subtract 和 multiply 的代码会被移除
// ========== sideEffects 配置 ==========
// package.json
{
"name": "my-app",
// false:表示所有模块都没有副作用,可以安全地 Tree Shake
"sideEffects": false
// 或者指定有副作用的文件(不会被 Tree Shake)
// "sideEffects": [
// "*.css",
// "*.scss",
// "./src/polyfills.js"
// ]
}
// ========== 什么是副作用? ==========
// 副作用:import 后会影响全局状态的代码
// ❌ 有副作用(即使没用到导出的值,导入本身就会执行代码)
import './polyfills' // 全局 polyfill
import 'normalize.css' // 全局样式
Array.prototype.myMethod = function() {} // 修改原型
// ✅ 无副作用(纯函数/纯组件,不使用就可以安全移除)
export const add = (a, b) => a + b
export const PI = 3.14159
// ========== Webpack 配置 ==========
module.exports = {
mode: 'production', // 自动开启 Tree Shaking
optimization: {
usedExports: true, // 标记未使用的导出
minimize: true, // 压缩时移除未使用代码
sideEffects: true // 读取 package.json 的 sideEffects 字段
}
}
💡 面试加分点: Tree Shaking 只对 ES Module 有效,因为
import/export是静态的(编译时确定),而 CommonJS 的require()是动态的(运行时确定),无法在编译阶段确定哪些代码被使用。
9. Webpack 的代码分割(Code Splitting)?
// 代码分割:将代码拆分成多个 bundle,按需加载,减少首屏加载量
// ========== 方式1:多入口(Multi Entry)==========
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js'
},
output: {
filename: '[name].[contenthash:8].js'
}
}
// ========== 方式2:动态导入(Dynamic Import)==========
// 最常用,Webpack 自动将动态导入的模块打包为单独的 chunk
// React 懒加载
const UserPage = React.lazy(() => import('./pages/UserPage'))
// Vue 路由懒加载
const routes = [
{ path: '/about', component: () => import('./views/About.vue') }
]
// 自定义 chunk 名称(魔法注释)
import(
/* webpackChunkName: "lodash" */
/* webpackPrefetch: true */ // 预获取:浏览器空闲时加载
'lodash'
).then(({ default: _ }) => {
console.log(_.join(['Hello', 'webpack'], ' '))
})
// ========== 方式3:SplitChunksPlugin(提取公共模块)==========
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 'async' | 'initial' | 'all'
minSize: 20000, // 最小 20KB 才分割
minChunks: 1, // 最少被引用 1 次
maxAsyncRequests: 30, // 按需加载时最大并行请求数
maxInitialRequests: 30, // 入口最大并行请求数
cacheGroups: {
// 提取 node_modules 中的第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10 // 优先级
},
// 提取公共模块
common: {
minChunks: 2, // 被至少 2 个 chunk 引用
name: 'common',
chunks: 'all',
priority: 5,
reuseExistingChunk: true // 复用已存在的 chunk
},
// 单独提取大型库
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
chunks: 'all',
priority: 20
}
}
},
// 将 Webpack 运行时代码单独提取
runtimeChunk: 'single'
}
}
💡 面试加分点:
webpackPrefetch(预获取)和webpackPreload(预加载)的区别——Prefetch 在浏览器空闲时加载(<link rel="prefetch">),Preload 与父 chunk 并行加载(<link rel="preload">)。
10. Webpack 的持久化缓存(Persistent Caching)?
// Webpack 5 内置了持久化缓存,大幅加速二次构建
module.exports = {
cache: {
type: 'filesystem', // 缓存到文件系统(默认是 'memory')
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
buildDependencies: {
config: [__filename] // 当 webpack.config.js 变化时,缓存失效
},
version: '1.0.0' // 改变版本号可手动使缓存失效
},
// 输出文件使用 contenthash 实现浏览器缓存
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].js'
}
}
// ========== 三种 hash 的区别 ==========
// [hash]:整个项目的 hash,任何文件变化都会改变
// [chunkhash]:基于 chunk 的 hash,同一 chunk 内文件变化才改变
// [contenthash]:基于文件内容的 hash,只有文件内容变化才改变(推荐)
// 场景:JS 文件用 contenthash,CSS 从 JS 中提取后也用 contenthash
// 这样修改 JS 不会导致 CSS 的缓存失效,反之亦然
构建速度对比(Webpack 5 持久缓存):
| 场景 | 无缓存 | 有缓存 |
|---|---|---|
| 首次构建 | 30s | 30s(首次仍需完整构建) |
| 无修改重建 | 30s | ~1s(从缓存恢复) |
| 小量修改 | 30s | ~3s(增量编译) |
11. Webpack 的 Module Federation(模块联邦)?
// 模块联邦:允许多个独立构建的应用之间共享模块
// 核心概念:应用既可以是"提供者"(expose),也可以是"消费者"(remote)
// ========== 应用 A(提供者):暴露模块 ==========
// app-a/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app_a', // 应用名
filename: 'remoteEntry.js', // 远程入口文件
exposes: {
'./Button': './src/components/Button', // 暴露的模块
'./utils': './src/utils/helpers'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
}
// ========== 应用 B(消费者):使用远程模块 ==========
// app-b/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app_b',
remotes: {
app_a: 'app_a@http://localhost:3001/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
}
// app-b/src/App.jsx 使用远程组件
const RemoteButton = React.lazy(() => import('app_a/Button'))
function App() {
return (
<React.Suspense fallback="加载中...">
<RemoteButton text="来自 App A 的按钮" />
</React.Suspense>
)
}
💡 面试加分点: Module Federation 是微前端的一种实现方案。它的优势是运行时共享——不需要将所有依赖打包到一起,应用可以在运行时动态加载远程模块。
shared配置可以避免重复加载 React 等公共依赖。
12. 如何优化 Webpack 构建速度?
module.exports = {
// ===== 1. 缩小搜索范围 =====
resolve: {
extensions: ['.ts', '.tsx', '.js'], // 减少扩展名尝试
alias: { '@': path.resolve(__dirname, 'src') },
modules: [path.resolve(__dirname, 'node_modules')] // 指定模块目录
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'), // 只处理 src 目录
exclude: /node_modules/, // 排除 node_modules
use: 'babel-loader'
}
]
},
// ===== 2. 持久化缓存(Webpack 5)=====
cache: {
type: 'filesystem',
buildDependencies: { config: [__filename] }
},
// ===== 3. 多线程/多进程 =====
module: {
rules: [{
test: /\.js$/,
use: [
{
loader: 'thread-loader', // 将耗时 loader 放到 worker 池中
options: { workers: 4 }
},
'babel-loader'
]
}]
},
// ===== 4. DLL 预编译(Webpack 4 常用,5 中用缓存替代)=====
// 将不常变化的库(React、Vue、lodash)预先打包
// ===== 5. 使用 esbuild-loader 替代 babel-loader =====
module: {
rules: [{
test: /\.[jt]sx?$/,
use: {
loader: 'esbuild-loader', // 比 babel-loader 快 10-100 倍
options: { target: 'es2020' }
}
}]
},
// ===== 6. 开发环境优化 =====
devtool: 'eval-cheap-module-source-map', // 快速 source map
optimization: {
removeAvailableModules: false, // 开发环境关闭
removeEmptyChunks: false,
splitChunks: false // 开发环境关闭代码分割
},
// ===== 7. 忽略大型库的解析 =====
module: {
noParse: /jquery|lodash/ // 不解析的库(已知没有依赖)
},
// ===== 8. TypeScript 类型检查放到独立进程 =====
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}
构建优化策略速查:
| 策略 | 效果 | 适用阶段 |
|---|---|---|
| 缓存(filesystem cache) | ⭐⭐⭐⭐⭐ | 开发+生产 |
| esbuild-loader | ⭐⭐⭐⭐⭐ | 开发+生产 |
| 缩小搜索范围 | ⭐⭐⭐ | 开发+生产 |
| thread-loader 多线程 | ⭐⭐⭐ | 生产 |
| 关闭开发环境不必要的优化 | ⭐⭐⭐⭐ | 开发 |
| noParse | ⭐⭐ | 开发+生产 |
13. 如何优化 Webpack 打包体积?
module.exports = {
mode: 'production', // 自动开启 Tree Shaking + 代码压缩
// ===== 1. 代码压缩 =====
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ['console.log']
},
mangle: true // 混淆变量名
}
}),
new CssMinimizerPlugin()
]
},
// ===== 2. 代码分割 =====
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: { test: /node_modules/, name: 'vendors', priority: 10 },
common: { minChunks: 2, name: 'common', priority: 5 }
}
}
},
// ===== 3. 外部依赖(CDN 引入)=====
externals: {
react: 'React',
'react-dom': 'ReactDOM',
lodash: '_'
},
// ===== 4. Gzip/Brotli 压缩 =====
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
})
],
// ===== 5. 图片压缩 =====
module: {
rules: [{
test: /\.(png|jpe?g|gif|webp)$/i,
use: [
{ loader: 'image-webpack-loader', options: {
mozjpeg: { progressive: true, quality: 65 },
pngquant: { quality: [0.65, 0.9] },
webp: { quality: 75 }
}}
],
type: 'asset'
}]
},
// ===== 6. 分析工具 =====
plugins: [
new BundleAnalyzerPlugin() // 可视化分析包体积
]
}
// ===== 7. 动态 polyfill =====
// 不在 bundle 中包含 polyfill,使用 CDN 按需加载
// <script src="https://polyfill.io/v3/polyfill.min.js?features=es2015"></script>
// ===== 8. lodash 按需引入 =====
// ❌ import _ from 'lodash' // 引入整个库 ~70KB
// ✅ import debounce from 'lodash/debounce' // 只引入需要的
// 或使用 babel-plugin-lodash 自动按需引入
14. 手写一个简单的 Loader?
// Loader 本质:一个函数,接收源文件内容,返回转换后的内容
// ========== 示例1:markdown-loader ==========
// markdown-loader.js
const marked = require('marked')
module.exports = function(source) {
// source 就是文件的原始内容
const html = marked.parse(source)
// 返回一个 JS 模块字符串
return `export default ${JSON.stringify(html)}`
}
// ========== 示例2:banner-loader(添加版权声明)==========
// banner-loader.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')
const schema = {
type: 'object',
properties: {
author: { type: 'string' },
date: { type: 'string' }
},
additionalProperties: false
}
module.exports = function(source) {
const options = getOptions(this)
// 验证配置
validate(schema, options, { name: 'Banner Loader' })
const banner = `/**
* @author ${options.author || 'Unknown'}
* @date ${options.date || new Date().toISOString()}
*/\n`
return banner + source
}
// 使用:
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.js$/,
use: {
loader: path.resolve('./banner-loader.js'),
options: { author: '张三', date: '2024-01-01' }
}
}]
}
}
// ========== 示例3:异步 Loader ==========
module.exports = function(source) {
const callback = this.async() // 声明异步操作
someAsyncOperation(source)
.then(result => callback(null, result)) // 成功
.catch(err => callback(err)) // 失败
}
15. 手写一个简单的 Plugin?
// Plugin 本质:一个具有 apply 方法的类
// apply 方法接收 compiler 对象,通过订阅钩子来扩展功能
// ========== 示例1:打包完成通知插件 ==========
class BuildNotifierPlugin {
constructor(options = {}) {
this.title = options.title || 'Webpack'
}
apply(compiler) {
// 订阅 done 钩子(编译完成时触发)
compiler.hooks.done.tap('BuildNotifierPlugin', (stats) => {
const time = ((stats.endTime - stats.startTime) / 1000).toFixed(2)
if (stats.hasErrors()) {
console.error(`❌ ${this.title} 构建失败!`)
} else {
console.log(`✅ ${this.title} 构建成功!耗时 ${time}s`)
}
})
}
}
// ========== 示例2:生成文件清单插件 ==========
class FileListPlugin {
constructor(options = {}) {
this.filename = options.filename || 'filelist.md'
}
apply(compiler) {
// 订阅 emit 钩子(输出文件到 dist 之前)
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
let content = '# 打包文件清单\n\n'
for (const filename in compilation.assets) {
const size = compilation.assets[filename].size()
const sizeKB = (size / 1024).toFixed(2)
content += `- **${filename}** (${sizeKB} KB)\n`
}
// 将新文件添加到输出资源中
compilation.assets[this.filename] = {
source: () => content,
size: () => content.length
}
callback()
})
}
}
// ========== 示例3:清除 console 插件 ==========
class RemoveConsolePlugin {
apply(compiler) {
compiler.hooks.compilation.tap('RemoveConsolePlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'RemoveConsolePlugin',
stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE
},
(assets) => {
for (const name in assets) {
if (name.endsWith('.js')) {
const source = assets[name].source()
const cleaned = source.replace(/console\.(log|warn|info)\([^)]*\);?/g, '')
compilation.updateAsset(name, new webpack.sources.RawSource(cleaned))
}
}
}
)
})
}
}
// 使用
module.exports = {
plugins: [
new BuildNotifierPlugin({ title: '我的项目' }),
new FileListPlugin({ filename: 'files.md' }),
new RemoveConsolePlugin()
]
}
💡 面试加分点: Webpack 的钩子系统基于 Tapable 库,有同步钩子(
tap)、异步串行钩子(tapAsync/tapPromise)、异步并行钩子等。常用钩子:beforeRun→run→compilation→make→emit→done。
16. Webpack 的 Proxy 代理配置?
// devServer.proxy 用于开发环境解决跨域问题
module.exports = {
devServer: {
port: 3000,
proxy: {
// 基本用法:将 /api 开头的请求代理到后端服务器
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, // 修改请求头中的 Origin
pathRewrite: {
'^/api': '' // /api/users → http://localhost:8080/users
}
},
// 多个代理
'/auth': {
target: 'http://localhost:9000',
changeOrigin: true
},
// WebSocket 代理
'/ws': {
target: 'ws://localhost:8080',
ws: true
},
// 条件代理
'/graphql': {
target: 'http://localhost:4000',
changeOrigin: true,
// 自定义代理逻辑
bypass: function(req, res, proxyOptions) {
if (req.headers.accept?.includes('html')) {
return '/index.html' // 返回 HTML 不代理
}
}
}
}
}
}
// 原理:
// 1. 浏览器请求 http://localhost:3000/api/users
// 2. Webpack Dev Server 拦截 /api 开头的请求
// 3. 使用 http-proxy-middleware 将请求转发到 target
// 4. 后端返回数据 → Dev Server → 浏览器
// 因为代理发生在服务端,不存在浏览器的同源策略限制
17. Webpack 5 相比 Webpack 4 有哪些重大变化?
| 特性 | Webpack 4 | Webpack 5 |
|---|---|---|
| 缓存 | 需要 cache-loader | 内置持久化缓存 |
| 资源处理 | file-loader/url-loader | 内置 Asset Modules |
| 模块共享 | 无 | Module Federation |
| Tree Shaking | 基础支持 | 嵌套 Tree Shaking + 内部模块 |
| 代码生成 | ES5 only | 可配置 ES6+ |
| Node polyfill | 自动注入 | 不再自动 polyfill |
| 构建速度 | 一般 | 大幅提升(缓存) |
| 包体积 | 一般 | 更好的优化 |
// Webpack 5 新特性示例
// 1. 持久化缓存
module.exports = {
cache: { type: 'filesystem' }
}
// 2. Asset Modules(替代 file-loader/url-loader)
module.exports = {
module: {
rules: [
{ test: /\.png$/, type: 'asset/resource' }, // 替代 file-loader
{ test: /\.svg$/, type: 'asset/inline' }, // 替代 url-loader(base64)
{ test: /\.txt$/, type: 'asset/source' } // 替代 raw-loader
]
}
}
// 3. Top-level await
// 可以在模块顶层使用 await
const data = await fetch('/api/config').then(r => r.json())
export { data }
// 4. 不再自动 polyfill Node.js 模块
// 如果代码中用了 Buffer、process 等,需要手动处理
module.exports = {
resolve: {
fallback: {
buffer: require.resolve('buffer/'),
process: require.resolve('process/browser')
}
}
}
18. Webpack 的 Resolve 配置详解?
module.exports = {
resolve: {
// 1. 省略扩展名:import './App' 会依次尝试 .ts .tsx .js .jsx
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
// 2. 路径别名
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
// 精确匹配
'lodash$': 'lodash-es' // import 'lodash' → 实际引入 lodash-es
},
// 3. 模块搜索目录
modules: [
path.resolve(__dirname, 'src'), // 先搜索 src
'node_modules' // 再搜索 node_modules
],
// 4. 主文件名(目录导入时的默认文件)
mainFiles: ['index'],
// 5. 条件导出(package.json 的 exports 字段)
conditionNames: ['import', 'require', 'browser', 'default'],
// 6. 符号链接
symlinks: false // 关闭符号链接解析(加速构建)
}
}
19. Webpack 的 DevServer 常用配置?
module.exports = {
devServer: {
port: 3000,
host: '0.0.0.0', // 允许局域网访问
open: true, // 自动打开浏览器
hot: true, // 热更新
compress: true, // Gzip 压缩
// SPA 路由支持
historyApiFallback: {
rewrites: [
{ from: /^\/admin/, to: '/admin.html' },
{ from: /./, to: '/index.html' }
]
},
// 静态资源目录
static: {
directory: path.join(__dirname, 'public'),
publicPath: '/static'
},
// 自定义中间件
setupMiddlewares: (middlewares, devServer) => {
// 在其他中间件之前添加
middlewares.unshift({
name: 'custom-middleware',
path: '/api/mock',
middleware: (req, res) => {
res.json({ message: 'mock data' })
}
})
return middlewares
},
// HTTPS
// server: 'https',
// 客户端配置
client: {
overlay: {
errors: true,
warnings: false
},
progress: true
}
}
}
20. 在实际项目中如何配置 Webpack(完整生产配置示例)?
// webpack.common.js(公共配置)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
clean: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: { '@': path.resolve(__dirname, 'src') }
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'esbuild-loader',
options: {
target: 'es2020',
jsx: 'automatic'
}
}
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
generator: { filename: 'images/[name].[hash:8][ext]' }
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' })
]
}
// webpack.dev.js(开发配置)
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
output: { filename: '[name].js' },
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }
},
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] },
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'] }
]
}
})
// webpack.prod.js(生产配置)
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
devtool: false,
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].js'
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }),
new CompressionPlugin({ algorithm: 'gzip', threshold: 10240 })
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({ parallel: true, terserOptions: { compress: { drop_console: true } } }),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: { test: /node_modules/, name: 'vendors', priority: 10 },
common: { minChunks: 2, name: 'common', priority: 5, reuseExistingChunk: true }
}
},
runtimeChunk: 'single'
},
cache: { type: 'filesystem' }
})
💡 面试加分点: 实际项目中使用
webpack-merge将配置拆分为公共/开发/生产三份,开发环境用style-loader(快),生产环境用MiniCssExtractPlugin.loader(提取 CSS 文件)。