跳到主要内容

webpack✅

有用过 webpack 或其他打包工具么,它们有什么特点解决什么问题?

答案
  1. 打包工具解决的核心问题

    问题类型具体解决方案
    模块化问题- 支持多种模块规范(CommonJS/ESM/AMD)- 处理模块依赖- 解决模块加载顺序
    兼容性问题- 语法转换(ES6+ → ES5)- Polyfill 注入- 浏览器适配
    性能优化- 代码压缩- Tree Shaking- 代码分割- 按需加载
    开发效率- 热更新- 开发服务器- 构建速度优化- 开发体验改善
    资源处理- 各类资源模块化- 资源转换和优化- 静态资源导入
  2. 主流打包工具对比

    打包工具主要特点适用场景
    Webpack- 生态最完善- 配置灵活- 功能强大- 支持多种模块规范- 大型复杂项目- 需要深度定制- 老项目维护
    Vite- 开发启动快- 热更新快- 配置简单- 基于ESM- 中小型项目- 新项目- 追求开发体验
    Rollup- 输出更纯净- Tree-shaking 好- 插件机制简单- 打包类库- SDK开发- 工具库
    Rspack- Rust实现,速度极快- Webpack兼容API- 内置开箱即用优化- 极少的配置- 对构建性能要求高- 大型项目迁移优化- 希望兼容Webpack生态
    ESBuild- Go语言编写,超高性能- 极简设计,专注速度- 最小化配置需求- 支持JS/TS/CSS- 构建工具底层依赖- 简单项目打包- 对速度极致要求场景
  3. 选型建议

    场景推荐工具原因
    大型应用Webpack生态完善、配置灵活、功能全面
    中小项目Vite启动快、配置简单、开发体验好
    类库开发Rollup输出清晰、体积小、Tree-shaking 优秀

延伸阅读

webpack 的主要配置项有哪些

答案
  1. entry: 配置模块的入口路径
    • 注意除了常规的配置文件路径,entry 也支持数组或者函数,一般数组用来实现 MPA 或者库项目打包为不同格式,函数则提供更加定制化的能力
  2. output 配置 webpack 的输出路径,和输出资源名称
  3. Loaders 扩展webpack 资源处理能力,支持类似 jsx、ts、css 图片等其他资源处理
  4. Plugins plugin 本质也是 loader ,只是职责上 loader 是处理资源,plugin 则用来处理其他类型的打包任务,比如打包优化、资源管理、注入环境变量等
  5. 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 生成的输出

    此外 output 也支持一些 占位策略

提示

参考 github 对 chunk 概念的讨论 chunk 也可按照功能分为

  • Entry Chunk 包含启动代码的 Chunk
  • Initial Chunk 非 Entry Chunk, 同步加载内容
  • Normal Chunk 异步加载的代码,Normal Chunk。

延伸阅读

loader 和 plugin 是什么,有什么区别, 用过哪些 loader 和 plugin

答案
对比项LoaderPlugin
作用转换单个文件,如 JS、CSS、图片等扩展 Webpack 编译生命周期,修改构建流程
执行时机在模块解析阶段(ModuleGraph 生成时)在整个 Webpack 生命周期中运行
影响范围影响单个文件影响整个构建过程
使用方式module.rulesplugins
示例babel-loader, css-loaderMiniCssExtractPlugin, HtmlWebpackPlugin

常用的 loader

  1. 转译器

  2. 模板引擎

  3. 样式处理

  4. 框架支持

提示

注意对于 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拆分代码块,以大幅提升构建速度
EnvironmentPluginDefinePlugin 的简写形式,可直接用于 process.env 变量
EslintWebpackPluginWebpack 的 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

延伸阅读

webpack 原理

答案

关键流程分为三个阶段

  1. 生成 ModuleGraph,基于传入的入口文件,递归解析所有依赖,形成依赖树。其中的核心步骤包括
    1. 读取入口文件内容,将其解析为 AST,读取 AST 中的依赖节点,递归解析依赖

      注意这里对于 js 或 ts 会采用 Acorn 解析依赖,其他资源会基于 loader 处理

    2. 基于配置的 loader 对匹配的文件进行转换,如将 ES6 转换为 ES5
  2. 生成 ChunkGraph 基于解析的依赖树,生成 chunk,其中的核心步骤包括
    1. 根据入口文件和依赖关系,生成 chunk
    2. 根据配置的插件,对 chunk 进行优化,如代码压缩、拆分、按需加载等
  3. 输出 bundle,将 chunk 输出为 bundle 文件,其中的核心步骤包括
    1. 将 chunk 转换为对应宿主环境的 bundle 文件
    2. 输出 bundle 文件到指定目录
提示

上面的核心数据流可以概括为 入口文件文本流 -> (生成模块依赖图) ModuleGraph -Loader 处理-> ChunkGraph -各种优化-> bundle 输出

如下是一个 mvp 版本的 webpack 实现,实现了基本的依赖解析和打包功能

const path = require('path')
const Compiler = require('./src/Compiler')
const babel = require('@babel/core')

// 示例 loader
const babelLoader = (source) => {
  // 使用 @babel/core 将 ES6 模块转换为 CommonJS 模块
  const result = babel.transformSync(source, {
    presets: [
      ['@babel/preset-env', {
        modules: 'commonjs', // 将 ES6 模块转换为 CommonJS
        targets: {
          node: 'current'
        }
      }]
    ]
  })
  return result.code
}

// 示例 plugin
class ExamplePlugin {
  apply (compiler) {
    compiler.hooks.emit.tap('ExamplePlugin', (compilation) => {
      console.log('ExamplePlugin: 资源生成完成')
    })
  }
}

// 创建编译器实例
const compiler = new Compiler({
  entry: path.resolve(__dirname, './fixture/entry.js'), // 修复路径
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [babelLoader]
      }
    ]
  }
})

// 应用插件
new ExamplePlugin().apply(compiler)

// 开始编译
compiler.run()

Open browser consoleTerminal

延伸阅读

  1. how webpack work webpack 作者对 webpack 原理讲解,涉及了更多细节
  2. Chunk Graph Algorithm webpak 作者对 chunk graph 的实现细节讲解
  3. 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 封装的一些启动代码,来确保打包后的内容能够在浏览器中正常运行,核心逻辑包括:

  1. 模拟 commonjs 的 require 函数,处理模块加载
  2. 判断模块依赖和加载顺序的相关函数, 比如 webpackJsonpCallback、同步、异步的 chunk 处理函数等
  3. 工具函数
  1. 非 runtime.js 构建后代码,本质就是往一个全局数组里推入 chunk 信息。以 main.js 输出的代码为例,可简化为
(self.webpackChunk = self.webpackChunk || []).push([
// 对应输出的chunkId
['main'],
// 该 chunk 对应的 module map
{
// 依赖模块代码
'./src/main.js': () => {},
'./src/vendor.js': () => {}
},
// 该 chunk 运行是函数,一般 main 会存在
() => {}
])
  1. 其他模块的输出同理,不论 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 信息
]
  1. 在执行到 runtime.js 的逻辑时,就是一个 IIFE,核心逻辑为
    1. 注入一系列状态和函数
    2. runtime.js 会尝试加载 main.js 中绑定的运行函数,如果存在依赖则将 main.js 对应的 chunk 推入一个延迟数组中
    3. 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))
})()
/************************************************************************/
})()

了解 tapable 么?

答案

tapable 是一个工具包。 webpack 基于它实现了底层的插件体系和事件流控制功能。 tapable 核心功能如下

  1. 插件能力,通过创建 Hook, 插件绑定 Hook 事件。
  2. 事件流控制能力,通过 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 优化可分为两大类:开发构建优化(提升构建速度)和生产构建优化(减小产物体积、提升运行性能)。

  1. 开发构建优化

    1. 缩小模块搜索范围

      • include/exclude 缩小 loader 处理范围(排除 node_modules)
      • resolve.extensions 设置合理的后缀查找顺序
      • resolve.alias 配置路径别名减少查找步骤
      • resolve.modules 指定第三方模块目录位置
    2. 利用缓存提升二次构建速度

      • babel-loader 配置 cacheDirectory: true
      • cache-loader 缓存其他 loader 结果
      • Webpack 5 的 cache: { type: 'filesystem' } 持久化缓存
    3. 并行与预编译

      • thread-loader 开启多进程处理耗时任务
      • 使用 DllPlugin 预编译稳定依赖
    4. 开发体验优化

      • 合理配置 devServer
      • 启用 HMR 热模块替换
  2. 生产构建优化

    1. 文件压缩与优化

      • JS: 使用 TerserPlugin 压缩
      • CSS: 使用 CssMinimizerPlugin 压缩,搭配 MiniCssExtractPlugin 提取独立CSS
      • HTML: 配置 HtmlWebpackPluginminify 选项
      • 图片: 使用 image-webpack-loader 压缩图像资源
      • 启用 Gzip/Brotli 压缩: CompressionWebpackPlugin
    2. 代码分割与按需加载

      • 配置 splitChunks 抽取公共代码
      • 使用 import() 实现动态导入,配合 webpackChunkName 合理命名
      • 基于 prefetch/preload 预加载关键资源
    3. 打包体积优化

      • 启用 Tree Shaking (production 模式自动启用)
      • CSS Tree Shaking: PurgecssWebpackPlugin
      • 使用 externals 排除不需打包的库
      • ModuleConcatenationPlugin 启用作用域提升
    4. 性能分析与监控

      • webpack-bundle-analyzer 分析包体积
      • speed-measure-webpack-plugin 检测构建速度瓶颈
  3. 其他策略

    1. 差异化构建

      • 现代浏览器使用ES6+模块,旧浏览器使用ES5兼容版本
      • 使用 browserslist 精准控制目标环境
    2. 模块联邦

      • 适用于微前端场景的依赖共享与动态加载

延伸阅读

说下异步加载

答案
  1. 解决什么问题 动态加载(按需加载)主要解决大型应用初始加载性能问题。通过将代码分割成小块并在需要时才加载,可以减少初始加载时间、提高首屏渲染速度,并优化资源利用率。

  2. 如何使用

    1. 采用 import() 语法进行动态加载, 魔法注释 webpackChunkName 可控制生成的 chunk 名称,还支持 webpackPrefetchwebpackPreload 等优化导入行为。
    2. 使用 React.lazySuspense 进行组件懒加载,结合 import() 实现按需加载。
    3. Vue 3.x 中使用 defineAsyncComponent 实现异步组件加载。
    // 使用 ES6 动态导入(推荐方式)
    import(/* webpackChunkName: "my-chunk" */ './module')
    .then(module => {
    // 使用模块
    })

    // 与 React.lazy 结合使用
    const MyComponent = React.lazy(() => import('./MyComponent'))
  3. 原理

webpack 的动态加载核心原理包括:

  • 构建时拆分:遇到 import() 语句时,webpack 将其识别为分割点
  • 分包生成:分离出独立的 chunk 文件而非并入主 bundle
  • 运行时加载:生成 JSONP 调用代码,在需要时创建 <script> 标签请求相应 chunk
  • 依赖解析:chunk 加载完成后自动解析并执行,提供给调用方使用

webpack 5 改进了缓存算法并优化了动态加载性能,提供更高效的代码分割实现。

tree-shaking

答案
  1. 解决问题

    • 消除未使用代码("死代码")
    • 减小打包体积
    • 提高应用性能和加载速度
  2. 使用方法

    • 使用ES6模块语法(import/export)
    • webpack配置中设置mode: "production"
    • 若项目有副作用,可通过sideEffects配置标记
  3. 原理

    • 基于ES6模块静态特性的静态分析
    • webpack解析代码构建依赖图谱
    • 标记未使用的模块和导出
    • 在打包过程中移除未使用代码
  4. 注意事项

    • 主要对ES6模块有效(webpack 5已支持部分CommonJS)
    • 动态导入(import())会影响分析
    • 副作用代码会被保留(可用/*#__PURE__*/标记纯函数)
    • 条件引用、函数式编程方式可能导致分析失效
    • 确保package.json中合理配置sideEffects

sideEffects 属性的作用是啥

答案
  1. sideEffects 解决什么问题

    sideEffects 解决的是 tree shaking (摇树优化)中的副作用识别问题。它帮助 webpack 确定哪些模块在导入但未使用其导出内容时可以被安全移除,从而减小打包体积,提高应用性能。

  2. sideEffects 如何使用

    在 package.json 中配置:

    {
    "sideEffects": false // 所有文件都无副作用
    // 或
    "sideEffects": ["*.css", "*.scss"] // 指定有副作用的文件
    }
  3. 原理和注意事项

    • 原理: webpack 检查模块的 sideEffects 标记,确定哪些导入但未使用的代码可以被安全移除。
    • 注意事项:
      • 错误标记会导致必要代码被删除
      • CSS 文件、全局样式通常需标记为有副作用
      • polyfills 和全局修改代码必须标记为有副作用
      • 可结合 /*#__PURE__*/ 注释标记纯函数调用

webpack externals 是如何加载外部依赖的

答案
  1. 解决什么问题

    • 减小打包体积:将指定依赖排除在最终bundle之外
    • 避免重复打包:利用已通过其他方式加载的库(如CDN)
    • 优化加载性能:减少初始加载时间和浏览器解析负担
  2. 如何使用

    // 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>
  3. 原理

    • 构建时:webpack遇到被标记为external的模块引用时不会将其打包
    • 运行时:基于配置的映射关系确定如何获取依赖
      • 浏览器环境:查找指定的全局变量(window.jQuery)
      • Node环境:通过require加载外部模块
      • 不同模块系统(UMD/CommonJS/AMD):采用对应的解析策略

externals配置支持多种形式,可根据运行环境和模块类型灵活处理依赖加载方式。

DllPlugin

答案
  1. 解决问题

    • 加快构建速度:预编译稳定依赖,避免每次构建重复分析第三方库
    • 提高开发效率:减少开发环境构建时间
  2. 使用方法

    • 创建单独的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')
    })
  3. 原理

    • 将稳定依赖单独打包为动态链接库
    • 生成manifest清单记录模块映射关系
    • 主构建过程中直接引用预编译模块而非重新分析打包
  4. 局限与注意事项

    • 需要额外的构建步骤和配置
    • 依赖变更时需要重新构建DLL
    • Webpack 5内置缓存已能解决类似问题,使用场景减少

webpack5 Module Federation

答案
  1. 解决什么问题

    • 实现微前端架构中多个独立应用间代码共享
    • 避免重复打包相同依赖
    • 支持独立开发、部署和运行的子应用间动态模块共享
  2. 如何使用

// 暴露模块的应用
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
}
})

// 消费模块的应用
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js'
}
})
  1. 原理与适用范围

    • 基于运行时容器机制,动态加载远程模块
    • 在构建时创建模块映射,运行时按需加载
    • 适用于大型前端应用拆分、多团队协作开发
  2. 注意事项

    • 需处理共享依赖的版本兼容性
    • 可与Single-SPA等微前端框架结合使用
    • 首次加载可能增加网络请求,但提升整体应用性能

延伸阅读

webpack热更新原理是什么?

答案
  1. 解决什么问题 Hot Module Replacement (HMR) 解决了开发过程中完全刷新页面的问题。它允许在不刷新整个页面的情况下,只更新变更的模块,同时保留应用状态(如表单输入、滚动位置等),从而提高开发效率和体验。
  2. 如何使用 可通过以下两种方式开启:
    • 在 webpack 配置中添加 new webpack.HotModuleReplacementPlugin()
    • 使用 CLI 参数 webpack-dev-server --hot
  3. 热更新的核心原理
    1. 文件监听与编译: webpack-dev-server 使用 webpack 的 watch 模式监听文件变化,当文件变化时触发重新编译
    2. 服务端推送: 编译完成后,webpack-dev-server 通过 WebSocket 向客户端发送"hash"和"ok"消息
    3. 客户端请求更新:
      • 客户端接收到消息后,通过 AJAX 请求获取热更新清单 (.hot-update.json)
      • 根据清单,通过 JSONP 请求获取更新的模块代码 (.hot-update.js)
    4. 框架对热更新的处理
      1. react fast refresh
      2. vue vue loader hot reload

webpack-dev-server 的功能

答案

webpack-dev-server 解决项目本地开发调试问题,常用核心功能如下

  1. 热模块替换(Hot Module Replacement) 缩写 HMR:webpack-dev-server 可以监听源文件的变化,当文件发生改动时,它会自动重新编译和打包,保证开发过程中始终使用最新的代码。

  2. 代理和反向代理:webpack-dev-server 可以配置代理,用于解决前端开发中跨域请求的问题。和微前端等路由映射问题。

source map 配置和原理

答案

延伸阅读

22%