# 前端工程化

# Webpack 的构建流程是什么?

Webpack 的构建流程可以概括为以下几个步骤:

  • 1、初始化参数: 读取命令行参数和配置文件,比如 webpack.config.js,合并得到最终配置。

  • 2、启动编译器: 创建 Compiler 对象,开始整个编译流程。

  • 3、从入口开始分析依赖: 从 entry 出发,解析模块之间的依赖关系,递归构建依赖图。

  • 4、调用Loader处理不同资源: Webpack 本身只认识 JS/JSON,像 .css.less.vue、图片等资源需要经过 loader 转换。 比如:

    • babel-loader:把 ES6+ 转成兼容代码
    • css-loader:解析 CSS 中的 import/require
    • style-loader:把样式插入页面
    • ts-loader:处理 TypeScript
  • 5、调用 Plugin 扩展构建能力: Plugin 作用于整个构建生命周期,比如:

    • 生成 HTML
    • 提取 CSS
    • 压缩代码
    • 清理输出目录
    • 注入环境变量
  • 6、生成 Chunk: 根据依赖关系和优化策略,将模块组合成若干 chunk。

  • 7、输出产物: 把 chunk 转换成最终的 bundle 文件,输出到指定目录。

  • 8、如果是开发模式,启动 dev server: 监听文件变化,支持热更新。

一句话概括:
Webpack 的本质是:从入口递归构建依赖图,经过 Loader 转换和 Plugin 扩展,最终产出浏览器可运行的静态资源。

# Webpack 和 Vite 的核心区别是什么?

Webpack 和 Vite 都是前端构建工具,但设计思路不同。

Webpack

Webpack 的核心是基于依赖图进行打包
它会从入口文件开始分析模块依赖,然后把项目中的资源打成一个或多个 bundle。

特点:

  • 生态成熟,插件和 loader 非常丰富
  • 对复杂场景兼容性好
  • 适合大型项目和深度定制
  • 启动慢,尤其在大项目中,冷启动和重构建成本高

Vite

Vite 的核心思路是:
开发环境利用浏览器原生 ES Module,不先整体打包;生产环境再基于 Rollup 构建。

特点:

  • 开发时启动非常快
  • 热更新速度更快
  • 配置相对简单
  • 更适合现代前端项目
  • 对某些复杂历史兼容场景没有 Webpack 灵活

本质区别

  • Webpack:开发时也要先打包
  • Vite:开发时尽量不打包,按需编译和加载

所以 Vite 在开发体验上通常更好,而 Webpack 在深度定制和复杂生态兼容上仍然很强。

# 为什么 Vite 开发环境快,生产环境却还是要打包?

因为开发环境追求的是反馈速度,Vite 利用浏览器原生 ESM,做到按需加载和按需编译,所以启动快、热更新快。

但生产环境不能直接把大量零散模块原样发给浏览器,因为这样会有:

  • 请求过多
  • 加载顺序和兼容问题
  • 缓存与压缩不够理想

所以生产环境仍然需要打包、压缩、分包和优化。
这就是为什么 Vite 开发不强调预打包全部源码,但生产构建依然要借助 Rollup 输出优化后的产物。

# Loader 和 Plugin 的区别是什么?

Loader

Loader 用于处理模块内容的转换。 因为 Webpack 默认只能处理 JS/JSON,所以需要 loader 去处理其他类型的文件,或者对源码做语法转换。

比如:

  • babel-loader:把新语法转旧语法
  • sass-loader:把 Sass 转 CSS
  • url-loader/file-loader:处理图片资源

特点:

  • 作用对象是“某一类模块”
  • 执行时机偏向模块解析阶段
  • 本质上是一个函数,输入源码,输出转换后的源码

Plugin

Plugin 用于扩展 Webpack 整个构建生命周期的能力。
它不只是处理某个文件,而是可以介入编译、打包、输出等任意阶段。

比如:

  • HtmlWebpackPlugin:自动生成 HTML
  • MiniCssExtractPlugin:提取 CSS 为单独文件
  • DefinePlugin:定义环境变量

特点:

  • 作用范围更广
  • 基于 Webpack 的事件钩子机制工作
  • 能影响整个构建流程

总结:

  • Loader:做“转换”
  • Plugin:做“扩展”

Loader 更像文件翻译器,Plugin 更像构建流程的插件系统。

# 什么是 Tree Shaking?它是怎么实现的?

Tree Shaking 指的是删除未被使用的代码,从而减小打包体积。

# 实现原理

TIP

它依赖 ES Module 的静态结构特性。
因为 ES Module 的 import/export 是编译时可分析的,所以构建工具可以确定哪些导出被引用,哪些没有被引用。

例如:

// math.js
export function add(a, b) {
  return a + b
}

export function minus(a, b) {
  return a - b
}
1
2
3
4
5
6
7
8
import { add } from './math'
1

如果只用到了 add,理论上 minus 就可以在构建时被移除。

# 为什么 CommonJS 不适合 Tree Shaking?

TIP

因为 CommonJS 是运行时加载:

const module = require('./math')
1

它的依赖关系和导出内容更动态,不利于静态分析。

Tree Shaking 生效的条件:

  1. 使用 ES Module 语法
  2. 构建工具支持静态分析
  3. 代码没有副作用,或者正确声明 sideEffects
  4. 生产模式下开启压缩优化

SideEffects 是什么?

有些文件虽然没有被直接引用导出,但执行它会产生副作用,比如引入一个 CSS 文件或给全局对象打补丁。这种文件不能被错误删除。

package.json 里可以配置:

// 表示大部分文件都没有副作用,便于优化。  
{
  "sideEffects": false 
}

 // 显式列出有副作用的文件
{
  "sideEffects": [
    "./src/global.css",
    "./src/polyfill.js",
    "**/*.scss" 
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

总结
Tree Shaking 的本质是基于 ES Module 静态分析,移除未使用代码。

# 什么是代码分割(Code Splitting)?常见方式有哪些?

代码分割是指把一个大的打包文件拆分成多个小文件,按需加载,提升首屏性能。

# 为什么要代码分割?

如果所有代码都打到一个大包里:

  • 首屏加载慢
  • 不利于缓存
  • 某些用户根本不会访问的页面也会被提前加载

# 常见方式

  • 1、按入口拆分: 多页面应用中,不同页面有不同入口,天然会形成多个 bundle。

  • 2、按路由懒加载: 单页应用中最常见。 例如 React:

    const UserPage = React.lazy(() => import('./UserPage'))
    
    1

    这样只有访问该路由时才加载对应模块。

  • 3、提取公共依赖: 把多个页面共用的库单独拆出来,比如 React、Vue、lodash 等。

  • 4、动态导入: 通过 import() 实现按需加载。

    button.onclick = async () => {
      const module = await import('./dialog')
      module.open()
    }
    
    1
    2
    3
    4

优点

  • 降低首屏包体积
  • 提升加载速度
  • 提高缓存利用率
  • 支持按需加载

注意: 拆分并不是越细越好。拆太多会增加请求数量和调度开销,所以需要平衡。

# 什么是 HMR?它和页面刷新有什么区别?

HMR 是 Hot Module Replacement,中文叫模块热替换

它的作用是在开发环境下,修改代码后只替换发生变化的模块,而不是整页刷新。

普通刷新:

页面会整体重新加载,应用状态会丢失。

HMR:

只更新变更模块,尽量保留页面当前状态,比如表单输入内容、组件状态等。

HMR 的优势:

  • 提升开发效率
  • 减少等待时间
  • 保留应用上下文,调试更方便

实现原理:

  1. 开发服务器监听文件变化
  2. 重新编译变更模块
  3. 通过 WebSocket 通知浏览器
  4. 浏览器拉取更新模块并替换原模块
  5. 如果模块支持热更新,就局部替换;否则回退为整页刷新

补充: Vite 的热更新体验通常更快,因为它基于 ESM 按模块更新,而不是重新构建整个 bundle。

# Babel 的作用是什么?它的工作原理是什么?

Babel 的核心作用是:
把高版本 JavaScript 代码转换为低版本环境也能运行的代码。

例如把箭头函数、class、可选链等语法转成旧浏览器兼容代码。

# 工作流程

Babel 主要分三步:

  • 1、Parse:把源码解析成 AST(抽象语法树)

  • 2、Transform:对 AST 做转换,比如把箭头函数转换为普通函数

  • 3、Generate:把转换后的 AST 再生成新的代码

Babel 只能转语法,不一定能补 API 比如:

  • Promise
  • Map
  • Array.from

这些属于新 API,不是语法。
单靠 Babel 不够,还需要 polyfill,例如 core-js

# preset 和 plugin 的区别

  • plugin:处理某一个具体转换
  • preset:一组 plugin 的集合

常见的 @babel/preset-env 会根据目标浏览器自动选择需要的转换和 polyfill 策略。

# Babel 和 TypeScript 编译有什么区别?

两者都可以把代码转换成浏览器可执行的 JS,但侧重点不同。

Babel:

主要关注语法转换
它把新语法转成旧语法,但默认不做类型检查。

TypeScript 编译器(tsc):

既可以做类型检查,也可以做代码编译。
它会把 .ts 转成 .js

实际项目中常见做法:

  • TypeScript 负责类型检查
  • Babel 负责语法转换和兼容性处理

原因是 Babel 插件生态更丰富,和 Webpack/Vite 集成更成熟。

一句话总结:

  • Babel 更擅长“转语法”
  • TypeScript 更擅长“类型系统和类型检查”

# CommonJS、ES Module 有什么区别?

  • 1、加载时机不同:

    • CommonJS 是运行时加载
    • ES Module 是编译时静态分析
  • 2、语法不同:

    CommonJS

    const a = require('./a')
    module.exports = {}
    
    1
    2

    ESM

    import a from './a'
    export default {}
    
    1
    2
  • 3、值的拷贝/引用关系不同:

    • CommonJS 导出的是值的拷贝
    • ES Module 导出的是值的引用绑定
  • 4、Tree Shaking 支持不同:

    ESM 天然适合 Tree Shaking,因为依赖关系静态可分析。
    CommonJS 不利于 Tree Shaking。

  • 5、使用场景:

    • Node 早期大量使用 CommonJS
    • 现代前端构建体系更偏向 ES Module

补充回答 现在 Node 也支持 ESM,但在历史兼容上,CommonJS 仍然大量存在。

# TypeScript 在工程化里的价值是什么?

TypeScript 在工程化中的价值主要体现在以下几点:

  • 1、提升可维护性: 通过类型约束,代码语义更清晰,特别是大型项目和多人协作场景。

  • 2、提前发现问题: 很多错误在编译阶段就能发现,而不是等到运行时才报错。

  • 3、提升开发体验: 编辑器可以基于类型系统提供更准确的代码提示、跳转、重构、补全。

  • 4、增强接口协作能力: 前后端联调时,类型定义可以作为更清晰的契约。

  • 5、更利于重构: 有类型兜底,重构时更容易定位受影响范围。

但它也有成本:

  • 学习成本
  • 类型系统维护成本
  • 某些复杂泛型会影响可读性

所以TypeScript 不是为了“炫技”,而是为了在中大型项目中提升稳定性和协作效率。


# 前端打包体积优化有哪些常见手段?

# 代码层面

  • 删除无用代码
  • 合理使用 Tree Shaking
  • 避免重复引入大型依赖
  • 使用更轻量的替代库
  • 按需加载组件和功能模块

# 构建层面

  • 代码分割
  • 压缩 JS/CSS/HTML
  • 图片压缩
  • 提取公共依赖
  • 关闭 source map 或按环境区分

# 资源层面

  • 使用合适的图片格式,如 WebP、AVIF
  • 小图转 base64 要谨慎
  • 字体子集化
  • 静态资源走 CDN

# 依赖管理层面

  • 定期分析 bundle
  • 避免多个版本依赖共存
  • 对第三方库做 external 或 CDN 化(视场景而定)

# 加载策略层面

  • 路由懒加载
  • 预加载 / 预获取
  • SSR / SSG 优化首屏

总结:

优化打包体积不能只看“包小不小”,还要结合首屏路径、缓存命中率和实际用户访问链路综合判断。

# 你如何做首屏性能优化?

从减少资源、加快加载、提前渲染 三个角度来做。

# 减少首屏资源体积

  • 路由级代码分割
  • Tree Shaking
  • 压缩 JS/CSS
  • 减少首屏非必要依赖
  • 图片压缩和格式优化

# 提高资源加载效率

  • CDN 分发静态资源
  • HTTP 缓存
  • preload 关键资源
  • prefetch 次要资源
  • 使用 HTTP/2 或 HTTP/3

# 优化渲染链路

  • 减少阻塞渲染的脚本
  • CSS 放头部,非关键 JS 延后
  • SSR / SSG 输出首屏 HTML
  • 骨架屏提升感知速度

# 接口与数据层优化

  • 首屏接口合并
  • 减少串行请求
  • 服务端提前聚合数据
  • 对非核心数据延迟加载

# 运行时优化

  • 减少大计算阻塞主线程
  • 长任务拆分
  • 大列表虚拟滚动

我会结合 FCP、LCP、TTI、CLS 等指标来评估优化效果,而不是只凭感觉。

# 静态资源缓存策略怎么设计?

静态资源缓存策略的核心目标是:
让用户尽量复用旧资源,同时在代码更新后又能及时拿到新资源。

# 资源文件名带 hash

例如:

app.8d9f3a.js
1

当文件内容变了,hash 就会变,URL 变化,浏览器自然会重新请求。

# 对静态资源设置强缓存

对于带 hash 的 JS/CSS/图片资源,可以设置较长的强缓存时间,比如一年。

# HTML 不做强缓存或缓存时间很短

因为 HTML 通常是入口文件,它负责引用最新 hash 资源,所以要保证它尽快更新。

# 配合 CDN 缓存

静态资源部署到 CDN,提高加载速度和缓存命中率。

# 理想策略

  • HTML:不缓存 / 短缓存
  • JS / CSS / 图片:文件名 hash + 长期强缓存

# 什么是 source map?线上环境要不要开?

source map 是源码映射文件,它可以把压缩混淆后的代码映射回源码,便于调试。

作用

生产环境的代码通常经过压缩和打包,报错堆栈可读性很差。
有了 source map,可以更快定位到源码中的具体文件和行号。

线上要不要开? 这要看场景,不是简单的“开”或“不开”。

开启的好处

  • 方便排查线上问题
  • 错误监控平台能还原源码堆栈

风险

  • 暴露源码结构
  • 可能带来安全和知识产权风险
  • 增加构建产物体积

常见方案:

  • 不对公众暴露完整 source map
  • 上传 source map 到内部监控平台,如 Sentry
  • 浏览器用户无法直接访问,但监控平台可以解析报错

vite打包构建时生成source map(sourcemap: 'hidden':生成独立文件但不关联,安全且完整),然后使用sentryVitePlugin插件,实现上传后删除.map文件。

# ESLint 和 Prettier 的区别是什么?

ESLint

ESLint 的核心是代码质量检查和部分风格约束
它关注的是:

  • 未定义变量
  • 未使用变量
  • 不安全写法
  • 潜在 bug
  • 某些代码风格规则

Prettier

Prettier 的核心是代码格式化
它不关心业务逻辑是否有问题,只负责统一格式,比如:

  • 缩进
  • 引号
  • 分号
  • 换行
  • 行宽

一般是两者配合:

  • ESLint 负责“代码对不对、规范不规范”
  • Prettier 负责“代码长得整不整齐”

为了避免规则冲突,通常会使用相关配置让 ESLint 和 Prettier 协同工作。

# Git hooks 在前端工程里有什么作用?

Git hooks 是在 Git 某些操作节点自动执行脚本的机制。
前端工程中最常用的是提交前校验。

常见场景:

  • 1、pre-commit: 在提交代码前执行:

    • ESLint 检查
    • Prettier 格式化
    • Stylelint
    • 单元测试
    • lint-staged

    这样可以把低质量代码拦在本地。

  • 2、commit-msg: 校验提交信息格式,比如是否符合 Commits 规范:

    • feat:
    • fix:
    • docs:
    • refactor:

作用:

  • 保证代码质量
  • 保证提交记录规范
  • 降低 CI 无效失败
  • 强化团队协作一致性

常用工具

  • Husky
  • lint-staged
  • commitlint

# CI/CD 在前端项目中一般怎么做?

CI/CD 分成两部分:

CI:持续集成 开发者提交代码后,系统自动执行一系列校验流程,比如:

  • 安装依赖
  • 代码检查
  • 单元测试
  • 打包构建
  • 产物验证

目的是尽早发现问题,避免低质量代码进入主分支。

CD:持续交付 / 持续部署 在 CI 通过之后,自动把构建产物发布到测试环境、预发环境或生产环境。

前端常见流程:

  1. 开发提交代码到 Git 仓库
  2. 触发 CI 流程
  3. 执行 lint、test、build
  4. 构建静态资源产物
  5. 上传到对象存储 / CDN / 服务器
  6. 刷新 CDN 缓存或发布新版本
  7. 通知测试或自动上线

常见平台:

  • GitHub Actions
  • GitLab CI
  • Jenkins
  • 阿里云 / 腾讯云 DevOps 平台

前端 CI/CD 的关键价值:

  • 自动化,减少人工操作失误
  • 快速反馈问题
  • 保证环境一致性
  • 支持灰度、回滚和多环境发布

# 前端项目如何做质量保障?

前端质量保障通常是多层次的,不是只靠测试。

  • 1、编码前:

    • 合理的技术方案设计
    • 组件和模块职责清晰
    • 类型系统约束
  • 2、编码中:

    • ESLint / Prettier / Stylelint
    • TypeScript
    • 代码评审(Code Review)
  • 3、提交前:

    • Git hooks
    • 本地 lint/test
  • 4、集成阶段:

    • CI 自动校验
    • 单元测试
    • 集成测试
  • 5、上线前:

    • E2E 测试
    • 回归测试
    • 构建产物检查
    • 多环境验证
  • 6、上线后:

    • 错误监控
    • 性能监控
    • 埋点分析
    • 告警与回滚机制

总结:
质量保障是一个覆盖开发前、开发中、上线前、上线后的闭环体系。

# 你们项目为什么从 Webpack 迁移到 Vite?收益和风险是什么?

原因:

随着项目规模增大,Webpack 在开发环境下的冷启动和热更新速度变慢,影响开发效率。尤其是大型中后台项目,启动时间和改一处代码后的反馈时间都会变长。

收益:

  • 冷启动更快
  • 热更新速度更快
  • 配置更简洁
  • 更贴近现代前端标准 ESM
  • 开发体验更好

风险:

  • 某些 Webpack 插件没有完全等价替代
  • 老项目依赖链复杂,迁移成本高
  • 对 CommonJS、Node Polyfill、特殊构建逻辑可能要额外处理
  • 团队需要重新适应部分配置方式

落地策略:

  • 先在新项目试点
  • 梳理现有构建依赖和插件能力
  • 核心链路先验证
  • 保留回滚方案
  • 将一些历史包袱在迁移中顺便治理掉

# 一个前端项目越来越大、构建越来越慢,你会怎么排查和优化?

  • 1:先定位瓶颈 不能一上来就乱优化,要先分析慢在哪里:

    • 冷启动慢
    • 热更新慢
    • 生产构建慢
    • 某些页面打包特别慢
    • CI 构建慢(分析CI构建过程,看哪里慢)

    冷启动 & 热更新(Dev 阶段)慢
    vite项目:

    • 可在终端查看冷热启动耗时
    • 可用vite-plugin-inspect深挖。

    webpack项目:

    • 可用speed-measure-webpack-plugin查看loader和plugin的耗时

    生产构建 & 某些页面特别慢(Build 阶段)
    vite插件:

    • vite-plugin-progress:查看当前构建的模块
    • rollup-plugin-visualizer:查看哪些依赖包最占体积

    webpack插件:

    • speed-measure-webpack-plugin:查看loader和plugin的耗时
    • webpack-bundle-analyzer:查看哪些依赖包最占体积
  • 2:分析构建过程 可以从以下角度查:

    • 1)依赖是否过大: 是否引入了不必要的大型库,是否存在重复依赖、多版本依赖。

    • 2)Loader / Plugin 是否过多: 某些 loader / plugin 会明显拖慢构建。

    • 3)是否做了不必要的全量编译: 比如 Babel / TS 是否处理了过多的第三方依赖目录。

    • 4)source map 是否过重: 某些 source map 配置会显著拖慢生产构建。比如:

      • sourcemap:true
      • sourcemap:"inline"
    • 5)是否缺少缓存: 例如 Babel 缓存、Webpack 持久化缓存。

  • 3:优化策略

    • 升级构建工具或核心版本
    • 合理开启缓存
    • 缩小 loader 处理范围
    • 拆分构建任务
    • 减少不必要插件
    • 路由级懒加载
    • 公共依赖拆分
    • 使用更快的编译工具,如 esbuild / swc 替代部分 Babel 能力
    • 检查 CI 环境依赖安装和缓存策略
  • 4:验证效果
    优化不能只凭体感,要对比:

    • 启动耗时
    • 热更新时间
    • build 耗时
    • 产物大小
    • 页面核心指标

# 如何设计一个适合团队的前端脚手架?

设计脚手架时,我会重点考虑“统一、提效、可扩展”。

# 核心目标

  • 统一项目初始化方式
  • 沉淀团队最佳实践
  • 降低新项目搭建成本
  • 减少重复配置

# 脚手架通常包含

1)项目模板
如 React / Vue、TypeScript、移动端 / PC 端、中后台模板等。

2)基础工程能力

  • ESLint
  • Prettier
  • Git hooks
  • Commitlint
  • 环境变量方案
  • 路径别名
  • API 请求封装
  • 状态管理基础设施
  • 测试框架基础配置

3)构建和发布规范

  • 开发 / 测试 / 生产环境区分
  • 打包命令
  • CI 配置模板
  • Docker 或部署脚本模板

4)可扩展性

  • 支持插件化能力
  • 支持按业务场景生成不同模板
  • 便于团队后续迭代升级

# 关键点

脚手架不是把所有东西都塞进去,而是要控制复杂度。
好的脚手架应该降低门槛,而不是制造新的学习成本。

# 前端微前端算不算工程化?它主要解决什么问题?

算,微前端是前端工程化在大型组织协作场景下的一种延伸方案。

主要解决的问题:

  • 1、大型前端应用难以协作: 多个团队维护同一个巨型项目时,代码耦合严重,发布相互影响。

  • 2、技术栈升级困难: 老系统很难整体重构,微前端可以允许不同子应用逐步演进。

  • 3、独立开发与独立部署: 不同子应用可以由不同团队单独开发、测试、上线。

微前端的价值

  • 提升团队自治能力
  • 降低主应用单点风险
  • 支持增量迁移

代价

  • 架构复杂度提高
  • 子应用通信、样式隔离、路由协调、公共依赖共享都更复杂
  • 运维和监控成本上升

结论 微前端不是银弹,它适合大型组织和复杂业务,不适合小项目为了“追新”而强上。

# 为什么说工程化的核心不是“工具”,而是“规范和流程”?

因为工具只是手段,不是目的。
如果团队没有统一规范,再好的工具也可能被用得很混乱。

比如:

  • 有 ESLint,但规则没人维护
  • 有 CI,但没人看失败结果
  • 有测试框架,但没有测试标准
  • 有脚手架,但每个项目都自己改一套

工程化真正要落地,靠的是:

  • 统一规范
  • 标准流程
  • 自动化机制
  • 团队共识

所以工程化的核心不是装了多少工具,而是这些工具是否真正融入研发流程并产生价值。

前端工程化的目标主要有四个:提升开发效率、保障代码质量、降低维护成本、支撑稳定交付。

上次更新: 3/15/2026, 9:35:10 PM