概念理解
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
module bundle chunk
Module 是代码的最基本单元,代表着一个个独立的文件或模块,JS文件、CSS文件或者其他任何类型的资源。
Bundle 是将多个源代码文件(如 JavaScript、CSS 或其他资源文件)组合成一个或多个输出文件的过程。这些输出文件可以是单个最终的应用程序文件,也可以是拆分成更小的代码块,以便于按需加载和懒加载等优化。
Chunk 是由打包过程生成的较小份代码,它是一个独立的代码片段,可以按需加载和执行。这样可以避免加载整个应用程序的所有代码,从而实现性能优化。在 Vite 中,当使用代码分割(Code Splitting)功能时,会将代码分解成多个 chunk 文件。
入门使用
构建流程
Webpack 的构建流程可以分为以下几个主要步骤:
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归地进行编译处理。
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在 Webpack 中,常用的 Loader 包括但不限于以下几种:
- babel-loader:用于将 ES6+ 代码转换为 ES5 代码,以便在旧版浏览器中运行。
- css-loader:用于处理 CSS 文件,使其能够被 Webpack 打包。
- style-loader:将 CSS 代码注入到 DOM 中,通常与
css-loader一起使用。 - sass-loader:用于将 SCSS/SASS 文件编译为 CSS。
- file-loader:用于处理文件(如图片、字体等),并将其输出到构建目录。
- url-loader:类似于
file-loader,但可以将文件转换为 base64 URL,适用于小文件。 - ts-loader:用于将 TypeScript 代码编译为 JavaScript。
- eslint-loader:在打包前对 JavaScript 代码进行 lint 检查。
- postcss-loader:用于处理 CSS,通常与
autoprefixer等插件一起使用,以添加浏览器前缀。 - html-loader:用于处理 HTML 文件,通常用于处理模板文件。
Webpack 中常用的 Plugin 包括:
- HtmlWebpackPlugin:自动生成 HTML 文件,并自动注入打包后的资源。
- MiniCssExtractPlugin:将 CSS 提取到单独的文件中,而不是嵌入到 JS 文件中。
- CleanWebpackPlugin:在每次构建前清理输出目录。
- DefinePlugin:允许在编译时创建全局常量。
- HotModuleReplacementPlugin:启用模块热替换(HMR)。
- CopyWebpackPlugin:将文件或目录复制到构建目录。
- CompressionWebpackPlugin:对资源进行 Gzip 压缩。
- BundleAnalyzerPlugin:生成打包分析报告,帮助优化打包体积。
- TerserPlugin:用于压缩 JavaScript 代码。
- ProvidePlugin:自动加载模块,而不必到处 import 或 require。
Webpack 的构建结果优化方法主要包括以下几个方面:
-
代码分割(Code Splitting) :
- 使用
SplitChunksPlugin将公共代码提取到单独的 chunk 中,避免重复打包。 - 使用动态导入(Dynamic Imports)按需加载模块,减少初始加载时间。
- 使用
-
Tree Shaking:
- 通过 ES6 模块语法(
import/export)和 Webpack 的mode: 'production'配置,移除未使用的代码。
- 通过 ES6 模块语法(
-
压缩代码:
- 使用
TerserPlugin压缩 JavaScript 代码。 - 使用
CssMinimizerPlugin压缩 CSS 代码。
- 使用
-
缓存:
- 使用
cache配置启用持久化缓存,加速二次构建。 - 使用
contenthash或chunkhash生成文件名,利用浏览器缓存。
- 使用
-
优化资源加载:
- 使用
file-loader或url-loader处理图片、字体等资源,减少 HTTP 请求。 - 使用
image-webpack-loader压缩图片。
- 使用
-
减少打包体积:
- 使用
externals配置排除第三方库,通过 CDN 引入。 - 使用
bundle analyzer分析打包结果,优化依赖。
- 使用
-
优化构建速度:
- 使用
thread-loader或happypack多线程构建。 - 使用
DllPlugin预编译不常变动的库。
- 使用
提升构建速度
- 使用
speed-measure-webpack-plugin插件:测量每个插件和 loader 的耗时,找出性能瓶颈。 - 使用
cache-loader或babel-loader的缓存功能:缓存编译结果,避免重复编译。 - 使用
thread-loader:将耗时的 loader 放在多线程中执行,提升构建速度。 - 使用
DllPlugin和DllReferencePlugin:将不常变动的库提前打包,减少重复构建。 - 合理配置
resolve:减少模块查找时间,如设置extensions和alias。 - 使用
HardSourceWebpackPlugin:为模块提供中间缓存,加速二次构建。 - 减少
babel-loader的编译范围:通过exclude或include缩小编译范围。 - 使用
terser-webpack-plugin的多线程压缩:提升代码压缩速度。 - 优化
devtool配置:在开发环境中使用eval或cheap-module-eval-source-map,减少 source map 生成时间。 - 使用
webpack-bundle-analyzer:分析打包体积,优化依赖。
webpack热更新内部流程
-
监听文件变化:Webpack 使用
webpack-dev-server或webpack-dev-middleware监听源文件的变化。 -
重新编译模块:当检测到文件变化时,Webpack 只重新编译受影响的模块,而不是整个项目。
-
发送更新信息:通过 WebSocket 将更新信息(如模块 ID)发送给浏览器。
-
应用更新:浏览器端的 HMR 客户端接收到更新信息后,使用
webpack/hot/dev-server提供的 API 应用新的模块代码。 -
模块更新与状态保持:如果模块支持 HMR(即实现了对应的接口),它可以在更新时保持其状态,否则会触发页面刷新。
Vite 的 HMR 基本流程
-
原生 ES 模块:Vite 利用浏览器支持的 ES 模块,避免了打包步骤,加快了开发启动速度。
-
文件变化监听:使用
chokidar监听文件变化。 -
模块重载:当检测到文件变化时,Vite 仅重新加载受影响的模块,通过 WebSocket 通知浏览器。
-
浏览器更新:浏览器端接收到更新信息后,利用原生 ES 模块的动态
import功能重新加载模块,保持应用状态。
Treeshaking
Tree Shaking 是一种通过 静态分析 消除 JavaScript 项目中未使用代码(Dead Code)的优化技术。其核心思想是基于 ES6 模块的静态特性(import/export),通过构建依赖图谱并标记未使用的导出项,最终在打包时将其移除,从而减小最终输出文件的体积。
Tree Shaking 的实现原理 (1)依赖 ES6 模块的静态特性 Tree Shaking 依赖于 ES6 模块的静态语法(import/export),因为这些语法在编译阶段就能明确模块的依赖关系。例如:
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// main.js
import { add } from './math';
console.log(add(1, 2)); // 仅使用 add,subtract 会被标记为未使用
关键特性:
- 静态导入导出:依赖关系在编译时确定,而非运行时。
- 无副作用:未使用的代码不会影响程序行为,可安全删除。
(2)静态分析与死代码标记 打包工具(如 Webpack、Rollup)会通过以下步骤实现 Tree Shaking:
-
依赖图谱构建:分析模块间的 import/export 关系,构建依赖树。
-
未使用导出标记:通过遍历依赖图,识别未被引用的导出项。
-
代码消除:在压缩阶段(如 Terser)移除标记的代码。
示例:
// utils.js
export const log = () => console.log("This will be removed!");
export const greet = () => console.log("Hello, world!");
// app.js
import { greet } from './utils';
greet(); // log 会被标记为未使用
(3)与传统 DCE 的区别 传统 DCE(Dead Code Elimination)关注 不可达代码(如 if (false) { ... }),而 Tree Shaking 更关注 未被引用的代码。例如:
// 传统 DCE 可识别并删除
if (false) {
console.log("Dead code");
}
// Tree Shaking 可识别并删除
import { unusedFunction } from './module'; // 未被使用
源码解读
webpack核心完成了 「内容转换 + 资源合并」 两种功能,实现上包含三个阶段:
- 初始化阶段:「初始化参数」:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数**「创建编译器对象」:用上一步得到的参数创建
Compiler对象「初始化编译环境」:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等「开始编译」:执行compiler对象的run方法「确定入口」**:根据配置中的entry找出所有的入口文件,调用compilition.addEntry将入口文件转换为dependence对象 - 构建阶段:「编译模块(make)」:根据
entry对应的dependence创建module对象,调用loader将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理**「完成模块编译」**:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 「依赖关系图」 - 生成阶段:「输出资源(seal)」:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会**「写入文件系统(emitAssets)」**:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
面试题
webpack5+vue3+ts+代码规范构建企业级前端项目
需要做什么配置
base配置:
-
配置入口文件
-
配置出口文件
-
配置alias 别名
-
配置extensions 引入模块时不带文件后缀时
-
vue的loader
-
ts的打包规则
-
css的打包规则
-
postcss的打包规则, 给浏览器css加前缀
-
less的打包规则
-
图像的打包规则
-
媒体资源的打包规则
-
字体的打包规则
-
构建好的静态资源都引入到一个html文件
-
兼容各系统的设置环境变量的包、并注入到业务代码里面去
-
低版本js兼容垫片和babel.config.js配置
-
externals: 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量。
-
module.noParse: 匹配到设置的模块,将不进行依赖解析,适合jquery,boostrap这类不依赖外部模块的包
-
ignorePlugin: 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉
-
开发环境: 启动服务器、热更新、source-map
-
生产环境:打包持久化缓存
-
生产环境:生产环境打包的时候把public下内容复制到构建出口文件夹
-
生产环境:抽取css样式文件
-
生产环境:压缩js
-
生产环境:配置打包文件hash-js、css、图片媒体资源
-
生产环境:代码分割第三方包和公共模块
-
生产环境:tree-shaking清理未引用js、css
还可以加:
- 缩小loader作用范围
- 缩小模块搜索范围
- 构建耗时分析
- 分析webpack打包后文件的插件
- 多线程loader
- 生产环境:打包时生成gzip文件