万字webpack精华总结:那些面试官提问的实现原理你答上来了吗

webpack概述:

webpack是一款常见的前端构建工具。它通过分析项目中的模块依赖关系,将各类资源视为模块。对于这些资源,一部分是像 Html,Css,js等浏览器认识的资源,另一部分是 jsx,ts,vue等浏览器不认识的资源,这时候就需要用到 webpack 这种构建工具去打包,去将这些不认识的代码转为浏览器认识的代码。webpack使用的是js代码进行开发,基于Node平台去运行。

通过本篇文章能学到什么:

  • webpack是怎么工作的

  • webpack用到什么工具去做这些构建打包工作的

  • 常见优化性能的实现原理详解

webpack构建流程:

webpack的构建过程分为三部分:初始化,编译,输出。

初始化:

  1. 读取配置:通过配置文件(webpack.config.js)或合并命令行参数(--mode)读取配置

  2. 创建Compiler对象:负责构建整个生命周期(启动,监听,关闭)

  3. 加载插件:调用每个插件的apply方法,将插件挂载到Compiler的生命周期上的钩子上。

  4. 初始化默认配置:根据mode(development/production)设置默认优化规则

编译:

  1. 从入口文件开始构建依赖图:根据entry找到入口文件

  2. 解析模块:

    1. 调用Loader:对模块内容进行翻译,转换
    2. 生成AST:将代码转为抽象语法树,分析模块的依赖关系(如import,require)
    3. 递归处理依赖
  3. 生成模块记录:保存每个模块的信息(如源码,依赖路径,转换后的代码)

输出:

  1. 生成chunk:根据依赖图和配置的代码分割(splitChunks),将模块组合成 Chunk
  2. 优化chunk:执行插件定义的优化操作(如 TerserPlugin 压缩代码,SplitChunksPlugin 拆分公共代码)
  3. 生成最终文件:

将 Chunk 转为浏览器可执行的代码

根据output配置,将文件写入磁盘

总结:

  • 初始化:启动构建,读取并合并配置参数,加载plugin,实例化compiler

  • 编译:从入口文件触发,找到每个module串行调用对应的loader,再找到该模块依赖的模块,递归地进行编译处理

  • 输出:将编译后地module组合成chunk,将chunk转换为文件,输出到文件系统

webpack打包流程:

首先,到这大家肯定有一个疑问,构建工程和打包过程不是一个东西吗???

严格来说,构建过程强调的是从源码到产物的完整处理过程,而打包过程更侧重于最终生成文件的结果阶段

构建过程和打包过程的区别:

流程 定义 阶段覆盖范围
构建过程 从读取配置到生成最终产物的完整过程,包括初始化、编译、优化等所有步骤。 包含从启动到输出的全部阶段(广义)。
打包流程 通常指将处理后的模块(Module)组合成 Chunk,并输出为文件(Bundle)的具体过程。 更侧重于编译后的输出阶段(狭义)。

打包流程:

  1. 从 Module 到 Chunk

    1. 根据入口文件代码分割规则 (如 import() 动态导入或 splitChunks 配置),将关联的模块分组为 Chunk

    2. 每个 Chunk 包含:

      • 入口模块及其依赖的所有模块。
      • 运行时代码(Webpack 自执行的胶水代码,用于模块加载和管理)。
  2. 优化 Chunk

    1. Tree Shaking :删除未被使用的代码(仅 production 模式生效)。
    2. 代码压缩 :通过 TerserPlugin 压缩 JS,CssMinimizerPlugin 压缩 CSS。
    3. 作用域 提升(Scope Hoisting) :合并模块作用域,减少闭包代码体积。
    4. 公共代码提取 :通过 SplitChunksPlugin 拆分重复依赖(如 node_modules 中的库)。
  3. 生成 Bundle 文件

  1. 将 Chunk 转换为最终的可执行文件(Bundle),包括:

    • JS Bundle:包含模块代码和 Webpack 运行时逻辑。
    • CSS Bundle :若使用 MiniCssExtractPlugin,CSS 会被提取为独立文件。
    • 资源文件 :如图片、字体通过 file-loader 输出到指定目录。
  1. 写入文件系统

    1. 根据 output 配置,将 Bundle 和资源文件写入磁盘(如 dist/ 目录)。
    2. 生成辅助文件(如 manifest.jsonindex.html 或 SourceMap)。

webpack Loader

webpack默认值只认识 js 文件,需要 Loader 去充当"翻译官"的角色,去识别其他不认识的文件

Css-Loader

在我们模块化开发时,需要将 css 文件和 js 文件分隔开

我们若想让浏览器识别 css 文件,则需要两个步骤:

  1. 首先使用 css-loader 进行依赖图中 css 文件的识别,进行 css 文件的解析
  2. 再使用 style-loader 将 css 链接到 index.html 文件中,让 css 发挥出真正的效果
java 复制代码
module.exports={
        module:{
                rules:[
                        {
                                test:/.css$/, //解释见[1]
                use:[ //记录需使用的loader,注意这里是从后往前使用,即先使用css-loader再使用style---loader
                    {loader:style-loader}, //将css文件链接至index.html
                    {loader:css-loader} //解析css文件
                ]
                        }
                ]
        }
}
//[1]这里使用的是正则表达式,/ /代表正则表达式的开始与结束,.代表.(.在正则中有特定含义,需要用\转义),$表示需要以.css结尾。

Less-Loader

less-loader是用来解析 less 文件,分为三步:

  1. 首先使用 less-loader 将 less 文件转为 css 文件(需要借助 less 库进行转化)
  2. 再使用 css-loader 进行依赖图中 css 文件的识别,进行 css 文件的解析
  3. 最后使用 style-loader 将 css 链接到 index.html 文件中,让 css 发挥出真正的效果
css 复制代码
module.exports={
    module:{
        rules:[
            {
                test:"/.less$/",
                use:[
                    {loader:"style-loader"},
                            {laoder:"css-loader"},
                            {loader:"less-loader"}
                ]
            }
                
            ]
    }

}

Postcss-Loader

postcss是一个样式转换工具,对 css 进行转化和适配,比如加一些浏览器前缀以适配不同浏览器,将 px 转为 rem 或 vh。但完成上述功能需要 postcss 对应不同插件的支持:

  • Autoprefixer 和 PostCSS-preset-env 的区别
特性 Autoprefixer PostCSS-preset-env
功能范围 仅添加前缀 前缀+未来 Css 语法转换
Css 新特性支持 不支持 支持
配置复杂度 简单 较复杂
独立性 可单独使用 内置Autoprefixer
  • 二者适用场景

如果只需处理浏览器前缀,用 Autoprefixer(更轻量)。

如果需要使用 CSS 新特性(如嵌套、逻辑属性等),用 PostCSS-preset-env

两者通常一起使用:PostCSS-preset-env 负责语法转换,Autoprefixer 处理前缀(即使 preset-env 内置了 Autoprefixer,也可单独配置)。

Babel-Loader

Babel 是负责在低版本的浏览器环境中依然能运行 es6+ 这种新特性语法,但每个新特性都需要对应插件的支持。

perl 复制代码
module.exports={
    test:/.js$/,
    use:[
            {
                loader:"babel-loader",
                options:{
                    plugins:[ //babel使用的插件
                        "@babel/plugin-transform-arrow-functions",
                        "@babel/plugin-transform-block-scoping"
                    ]
                }
            }
    ],

}

然而这些插件又多又难记,因此实际开发中一般给 webpack 提供一个预设preset,webpack 根据预设来加载对应插件列表并传递给Babel:

javascript 复制代码
module.exports={
    test:/.js$/,
    use:[
            {
                loader:"babel-loader",
                options:{
                          presets:["@babel/preset-env"] //注意这里是预设preset配置项,已不再是plugins
                }
            }
    ],

}

webpack plugin

Plugin 概述:

loader代表加载器,只能完成不同模块类型的解析,plugin代表插件,可以做更加广泛的任务,贯穿于Webpack整个生命周期。

Plugin 的作用:

  1. 打包优化,将样式单独抽取为一个文件并在index.html中用link方式引用,而不是用style标签直接嵌入到html页面中。

  2. 资源管理,二次打包前先删除上次打包后的文件夹,可以使得二次打包不需要的文件(如图片)自动删除。

  3. 环境变量注入,使得变量一处定义,处处可用。

常见 plugin:

资源管理 clean 插件: clean-webpack-plugin 可以使得二次打包前先删除打包后的文件夹,即二次打包不需要的文件删除

java 复制代码
import {CleanWebpackPlugin} from "clean-webpack-plugin"
module.exports={
    plugin:[new CleanWebpackPlugin()]
}

Html 插件: 默认情况下,打包后只有一个 js 文件,然后需要手动添加一个 html 文件引入 js 文件。html-webpack-plugin 就是帮我们自动生成 html 文件的

java 复制代码
import {CleanWebpackPlugin} from "clean-webpack-plugin"
module.exports={
    plugin:[new CleanWebpackPlugin()]
}

DefinePlugin:define-plugin插件用于环境变量注入,使得变量一处定义,处处使用。该插件是 webpack 内置的插件,直接从 webpack 库中引入即可:

javascript 复制代码
const {DefinePlugin} from "webpack"
module.exports={
    plugins:[
        new DefinePlugin({
            "BASE_URL":"./" //配置BASE_URL变量的值为"./"
        })
    ]
}

自定义 plugin:

  1. 目的: 自定义 plugin可以让 webpack 做自己定制的需求,执行特定的任务,有更强的构建能力
  2. 工作原理:

Webpack 就像一条生产线,插件就像插入生产线的一个功能

Webpack 在编译代码过程中,会触发一系列钩子事件,插件做的就是找到对应钩子,往上面挂上自己的任务,也就是注册事件。

当 webpack 构建的时候,插件就会找到对应钩子触发执行了

  1. 理解 compiler 和 compilation:

compiler是贯穿了整个 webpack 生命周期的一个实例,代表了完整的 webpack 的环境配置

compilation是每次新的资源构建时创建的一个实例,代表一次单独的版本构建

  1. 怎么实现自定义 plugin:

搭建插件的基本结构:需要 apply 方法,接受 compiler 对象作为参数

在 apply 方法中注册生命周期钩子

生成版本文件 用 node 在 debugger 行设置断点进行调试

javascript 复制代码
//需求:给打包输出文件添加注释

const { compilation } = require("webpack");

//开发思路:
//触发时机:生成资源到输出目录之前 emit钩子
//如何获取打包输出的资源?compilation可以获取所有的即将输出的资源文件

class BannerWebpackPlugin{
    constructor(){}
    apply(compiler){
        compiler.hooks.emit.tap('BannerWebpackPlugin',(compilation)=>{
            debugger;
            const extensions=['css','js'];
            //过滤只保留这两种文件
            const assets=Object.keys(compilation.assets).filter((assetpath)=>{
                const splitted=assetpath.split('.');
                const extension=splitted[splitted.length-1];
                return extensions.includes(extension)
            })
            const prefix = '/* Built by BannerWebpackPlugin */\n';
            assets.forEach((asset)=>{
                const source=compilation.assets[asset].source();//原来内容
                const content=prefix+source; //拼接上注释
                //修改资源
                compilation.assets[asset]={
                    source(){
                        return content; //资源内容
                    },
                    size(){
                        return content.length
                    }
                }
            })
        })
    }
}

module.exports=BannerWebpackPlugin

plugin和loader的区别:

  • webpack只认识JavaScript,Loader就是充当一个翻译官的角色,对其他类型的资源做转译的预处理工作

  • Plugin就是插件,基于事件流框架Tapable,扩展Webpack的功能,贯穿于Webpack的整个生命周期

  • Loader在module.rules中配置,类型为数组,每一项都是Object

  • Plugin在plugins中单独配置,类型为数组,每一项都是一个plugin的实例,参数通过构造函数传入

webpack的常见优化手段整理:

  1. 并行构建优化:

多进程多实例构建:thread-loader

并行压缩:terser-webpack-plugin

  1. 缩小构建范围:

精确指定 loader 的作用范围

优化模块解析

排除无用模块

  1. 缓存利用:

持久化缓存:cache属性

Loader 缓存:babel-loader开启缓存

HardSourceWebpackPlugin插件

  1. 代码拆分与按需加载:
  2. Tree shaking 与 scope hosting
  3. 图片压缩:image-webpack-plugin
  4. 按需加载 polyfill

热更新HMR

  1. HMR 概述: 它能够让你在不刷新页面的前提下,自动更新修改的部分,同时还能保留页面状态
  2. HMR的核心就是客户端向服务器拉取更新后的文件,即 chunk diff
  3. 实际上 webpack-dev-server 与浏览器上维护了一个websocket,当本地资源变化后,wds会立刻向浏览器推送更新,并带上构建时的hash,与上一次的构建做对比。
  4. 客户端对比出差异后,会向WDS发送AJAX请求来获取这些更改内容,后续客户端就可以借助这些信息发送JSONP请求获取 chunk 的增量更新。

树摇 Tree shaking

Tree shaking 基于ESM的静态结构特性,通过分析模块间的导入导出关系,精确地识别并移除未被引用的代码

实现原理:

  1. 在编辑阶段,提取所有导出变量并记录到模块依赖图的结构中,构建完整的模块关系图并标记未使用的导出
  2. 随后,在优化阶段,Terser等压缩工具会识别并物理移除未使用的代码,最终生成精简的产物文件

为什么CommonJS 不支持 Tree shaking?

  1. CommonJS 模块系统允许高度动态的导入模式
  2. Cjs 导出的是完整的模块对象
  3. Cjs 无法在编译时确定使用了哪些具体导出项,依赖解析发生在执行阶段,模块依赖图只有在代码执行时才能完全确定
  4. 静态分析工具无法可靠地预测所有可能的模块加载路径

代码分割

  • 代码分割就是将一个大的bundle拆分成多个小的chunks

  • 代码分割的本质是:将代码拆解为更小的,可独立加载的模块单元,在全量加载和零散请求之间找到性能最优解,将可控的复杂度换取用户体验与工程效率的平衡

  • 要抽离的代码:

    • 公共模块的代码不需要重复打包,单独抽离成一个文件,直接引用即可
    • 第三方模块的代码,单独抽离成一个文件,然后比如lodash
  • 核心拆解:

    • 以一个电商网站的长页面为例,
    • 要首先做首屏加载
    • 再做交互时加载,后续分批按需加载
  • 意义:这样做既不会让浏览器内存超载,又可以逐步扩展功能

Source Map:

  1. Source Map 概述: Source Map 是一种用来还原源码位置的技术,记录了打包压缩后代码和源码之间的映射关系,让你在浏览器调试时看到真正的代码。
  2. 常见的环境搭配:
  1. 工作原理:

当在webpack的配置文件中配置了 Source Map,打包文件会产生一个魔法注释。这个魔法注释告诉浏览器,这份文件有 Source Map。浏览器看到这行文件后,会请求对应的 Map 文件,然后使用该 .map文件中的信息,进行还原,映射和调试。

其他常见优化场景:

开发环境优化

  • 增量构建
  • HMR
  • Source Map
  • 多进程多实例构建: Thread Loader

生产环境优化

  • 利用CDN
  • Tree shaking
  • 代码分割,拆包优化

构建服务端渲染SSR

  • 将客户端代码和服务端代码分开,减轻服务器负担
  • 什么是SSR:指的是在服务端产生HTML页面,然后将完整的HTML页面发送到浏览器进行展示的一种渲染方式
  • 与客户端渲染CSR相比,SSR在服务器上运行JS代码生成页面,而不是依赖浏览器在客户端运行代码生成页面
  • SSR的核心流程:

暂时无法在飞书文档外展示此内容

Babel 原理:

  • Babel是什么:

    • Babel就是允许开发者在低版本环境下使用最新的js语法
    • babel就是可扩展的插件系统,用来添加,删除,或转换规则
  • Babel的三种存在方式:

    • 使用单体文件
    • 命令行:就是在package.json的scripts命令中的某条命令
    • 构建工具的插件:webpack的babel-loader
  • 运行方式和插件:

    • Babel总共分为三个阶段:解析,转换,生成
    • Babel本身不具有任何转换功能,他把一个个转换的功能都分解到一个个plugin中,因此当我们不配置任何插件时,经过Babel的代码和输入是相同的
    • 在解析阶段:把源码转换成结构化的AST,在转换阶段:遍历AST转换为目标字符串
  • preset:一个功能一个插件,那岂不是要安装很多插件,设立预设就是提供了一组插件的集合

  • Babel原理:

    • 解析:将代码转换成AST
    • 词法分析:将代码分割成tokens流
    • 语法分析:分析token数组转换成AST树
    • 转换:遍历AST并生成新的AST
    • 生成:以新的AST为基础生成代码
相关推荐
weifexie30 分钟前
ruby可变参数
开发语言·前端·ruby
千野竹之卫31 分钟前
3D珠宝渲染用什么软件比较好?渲染100邀请码1a12
开发语言·前端·javascript·3d·3dsmax
sunbyte31 分钟前
初识 Three.js:开启你的 Web 3D 世界 ✨
前端·javascript·3d
半兽先生1 小时前
WebRtc 视频流卡顿黑屏解决方案
java·前端·webrtc
南星沐2 小时前
Spring Boot 常用依赖介绍
java·前端·spring boot
孙_华鹏3 小时前
手撸一个可以语音操作高德地图的AI智能体
前端·javascript·coze
zhangxingchao3 小时前
Jetpack Compose 动画
前端
@PHARAOH3 小时前
HOW - 缓存 React 自定义 hook 的所有返回值(包括函数)
前端·react.js·缓存
拉不动的猪3 小时前
设计模式之--------工厂模式
前端·javascript·架构
前端开发张小七3 小时前
16.Python递归详解:从原理到实战的完整指南
前端·python