前端工程化实践
Category(分类): Other Status: 已整理
本文在保留原文“从代码规范、构建、缓存、部署到 CI/CD 建立工程体系”这一主线的基础上,补充现代前端项目的依赖管理、测试、预览环境和可观测性实践。
原始链接(旧路径,站点可能已调整):前端工程化实践——山月行
可访问镜像:前端工程化实践
一、前端工程化到底解决什么问题
前端工程化不是把项目配置得越来越复杂,也不是工具越多越先进。它要解决的是一组长期、重复且容易出错的问题:
- 多人协作时如何保持代码风格和接口约定一致;
- 如何让依赖安装、开发、构建和发布可重复;
- 如何在合并代码前尽早发现类型、逻辑、兼容性和安全问题;
- 如何让每个分支都能被单独预览和验收;
- 如何知道一次发布是否真的改善了用户体验,并能在出问题时回滚。
一个可落地的工程化闭环通常是:
需求与设计
↓
代码与分支协作
↓
本地开发与自动检查
↓
Pull Request / Merge Request
↓
CI:安装、检查、测试、构建、扫描
↓
预览环境 → 灰度/生产发布
↓
监控、反馈、回滚与持续改进
工程化的目标是降低变化成本,而不是阻止变化。小团队可以只建立最小闭环,大团队再逐步增加共享配置、组件库、预览环境和发布平台。
二、原文内容与今天的对应关系
原文工程化系列主要覆盖以下主题,这些方向今天仍然有效:
- JavaScript 压缩、Tree Shaking 和打包体积分析;
- Webpack 等构建工具及代码分割;
- HTTP 缓存控制和 CDN;
- ESLint、代码规范和提交约束;
- 依赖安装速度、锁文件和 CI 缓存;
- Docker、Nginx、CI/CD、特性分支环境和灰度发布;
- 本地 HTTPS、图片处理、日志和异常监控。
需要更新的是:新项目通常优先使用框架官方方案、Vite、Rspack 或其他现代构建工具,不必为了追求“最新”迁移稳定的 Webpack 项目;npm i、npm ci、pnpm 和 Yarn 也应根据团队统一的包管理策略使用,不能在同一个项目中随意混用。
三、先统一项目基础
1. 固定运行时和包管理器
团队应明确 Node.js、包管理器和构建工具的支持范围,并把约定写入仓库:
- 使用
.nvmrc、.node-version或 CI 镜像固定 Node 主版本; - 在
package.json的engines中声明支持范围; - 使用
packageManager字段记录团队批准的包管理器和版本; - 提交且只维护一份锁文件,例如使用 pnpm 就提交
pnpm-lock.yaml; - CI 使用不可变安装,例如
pnpm install --frozen-lockfile或npm ci; - 升级 Node、包管理器和锁文件时通过 PR 评审,而不是让每个人本地自行升级。
示例:
{
"engines": {
"node": ">=20 <25"
},
"packageManager": "pnpm@<团队批准的固定版本>"
}
尖括号只是占位符,实际项目必须替换成已经验证过的具体版本。engines 是提示和约束工具是否检查它,并不能替代 CI 对运行时版本的固定。
2. 统一常用脚本
脚本名称应稳定、可发现,避免每个项目都使用不同命令:
{
"scripts": {
"dev": "vite",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"test:e2e": "playwright test",
"build": "vite build",
"preview": "vite preview"
}
}
不同框架的命令会有差异,例如 Nuxt、Next.js 和基于 Vite 的项目不一定使用完全相同的脚本。重要的是同一个脚本在本地、CI 和发布平台上含义一致。
3. 配置文件和环境变量
构建时环境变量会被替换进前端产物。只要变量会进入浏览器包、HTML 或公开运行时配置,就不能把它当作秘密。数据库密码、云服务私钥、JWT 签名密钥和第三方管理 Token 必须放在服务端或 CI Secret 中。
建议区分:
- 构建时配置:构建时注入,通常会进入产物;
- 运行时公开配置:通过服务端或同源配置接口提供给浏览器,只能放公开值;
- 服务端秘密:只在服务端、Secret Manager 或 KMS 中使用。
对配置做 schema 校验比在业务代码中到处读取字符串更可靠。缺少必须配置时,应在启动或构建阶段失败,而不是上线后才出现隐蔽错误。
四、代码质量:自动化约束,而不是口头约定
1. ESLint、格式化和类型检查
现代 ESLint 项目优先采用 Flat Config(通常为 eslint.config.js、eslint.config.mjs 或 eslint.config.ts),并按实际技术栈选择 Vue、React、TypeScript 等插件。不要把别人的规则文件完整复制过来;每条规则都应能说明它解决的风险。
通常可以组合:
- ESLint:发现潜在错误和约定问题;
- Prettier:统一格式,不承担复杂语义检查;
- Stylelint:检查 CSS、SCSS 等样式;
- TypeScript:检查类型关系和 API 契约;
vue-tsc或框架官方类型检查命令:补充模板类型检查;- markdownlint:在团队需要时检查文档。
不要只在提交前运行全部检查,否则反馈可能太慢。编辑器即时提示、提交前轻量检查和 CI 完整检查应分层安排。
2. Git Hook 和提交规范
Husky、lint-staged、Commitlint 或 Lefthook 等工具可以在提交前运行格式化、Lint 或提交信息校验。它们只是开发体验工具,本地 Hook 可以被跳过,不能作为安全边界;必须阻断的检查应在 CI 和分支保护中再次执行。
提交信息可以采用 Conventional Commits,例如:
feat: 增加文章搜索
fix: 修复移动端图片溢出
chore: 升级构建依赖
提交规范不是目的。对于小团队,如果提交规则带来的维护成本超过收益,可以采用更简单的约定;对于需要自动生成变更日志、发布包的团队,规范化提交会更有价值。
3. 代码评审
PR/MR 不应只检查格式,还应关注:
- 需求和异常状态是否覆盖;
- 权限、输入校验和敏感数据是否安全;
- 组件是否真的可复用,是否引入了不必要的抽象;
- 大型依赖、重复代码和包体积是否合理;
- 测试是否覆盖历史缺陷和关键路径;
- 是否需要迁移说明、文档或监控指标。
把可自动化的内容交给工具,把业务判断和架构取舍留给评审者。
五、模块化、构建和体积控制
1. 构建工具的选择
Webpack、Vite、Rspack、Rollup、Parcel 以及框架自带的构建工具各有适用场景:
- 应用项目需要开发服务器、热更新、代码分割和资源处理;
- 库项目更关心 ESM/CJS 输出、类型声明、Tree Shaking 和兼容性;
- SSR/SSG 项目应优先使用框架官方构建流程;
- 已经稳定运行的老项目,迁移前应先测量构建时间、开发体验和产物变化。
不要把“压缩后代码看不懂”当成安全措施。压缩、混淆和 WebAssembly 都不能隐藏交付到浏览器的秘密,权限和核心规则必须在服务端校验。
2. 代码分割和资源加载
合理的代码分割可以降低首屏 JavaScript,但分割过度也会增加请求、解析和调度开销。应结合真实路由和用户行为决定:
- 路由级懒加载;
- 大型编辑器、图表和地图等重组件按需加载;
- 对不会进入首屏的内容使用动态导入;
- 通过构建分析工具检查重复依赖和初始包体积;
- 对关键 CSS、字体和图片只做有依据的预加载。
Tree Shaking 依赖 ESM 的静态结构和正确的副作用声明,不能保证任意 CommonJS 包都被完美删除。应通过产物分析确认优化是否生效。
3. 资源缓存
静态资源使用内容哈希文件名时,可以设置较长的缓存时间:
Cache-Control: public, max-age=31536000, immutable
HTML 入口和运行时配置通常需要短缓存或重新验证,不能把 immutable 盲目加到所有响应。删除或修改文件时,应通过新的哈希文件名、发布清单和 CDN 缓存策略保证新旧版本切换可控。
ETag、Last-Modified 和 If-None-Match 适合做缓存验证,但它们不是让资源“永远不过期”的办法。个性化响应应谨慎使用共享缓存,必要时使用 private。
六、依赖管理与 Monorepo
1. 依赖不是越多越好
引入依赖前至少评估:
- 是否真的减少了代码和维护成本;
- 包的体积、模块格式和浏览器兼容性;
- 维护活跃度、漏洞、许可证和供应链风险;
- 是否有类型声明、文档和可替换方案;
- 未来升级和出现问题时由谁负责。
不要为了统一而保留无人维护的内部工具,也不要在多个项目中复制一份稍有差异的配置而无人升级。
2. 工作区和共享包
pnpm、npm 和 Yarn 都提供 Workspace 能力。Monorepo 适合共享组件、设计令牌、ESLint 配置、API 类型和工具包,但它也会带来构建边界、发布版本、权限和 CI 缓存等复杂度。
共享包至少应有:
- 清晰的入口和导出边界;
- 类型声明、文档和测试;
- 独立的版本与变更记录;
- 明确的运行时和 peer dependency;
- 消费者升级失败时的回滚方案。
可以使用 Changesets 等工具管理多包变更,但不要把 Monorepo 当作所有项目的默认答案。多个项目没有稳定共享关系时,独立仓库反而更简单。
3. 供应链安全
CI 中可以加入依赖漏洞、许可证、机密信息和恶意脚本扫描。锁文件能固定解析结果,但不能保证依赖永远安全;应关注维护者变更、发布包变化、安装脚本和 CI Token 权限。
生产构建尽量在干净环境完成,并保留构建日志、SBOM 或依赖清单。Source map 有助于定位线上错误,但可能包含源码和路径信息,应上传到受控的错误监控平台,不要默认公开。
七、测试体系:从便宜到昂贵
测试策略不应只追求覆盖率数字,而要保护用户最重要、最容易回归的行为:
| 层级 | 适合检查的内容 | 常见工具 |
|---|---|---|
| 静态检查 | 语法、风格、类型和明显错误 | ESLint、TypeScript、Prettier |
| 单元测试 | 纯函数、状态转换和边界 | Vitest、Jest |
| 组件测试 | 交互、状态和可访问性 | Testing Library、Storybook |
| 视觉测试 | 主题和关键页面回归 | Storybook、Chromatic 等 |
| 端到端测试 | 登录、搜索、支付等关键流程 | Playwright、Cypress |
| 契约测试 | 前后端接口兼容性 | OpenAPI、JSON Schema 等 |
| 线上监控 | 真实错误、性能和转化 | RUM、错误监控、日志平台 |
测试应关注用户可见行为,避免大量断言私有实现细节。端到端测试不必覆盖所有页面,但应覆盖高价值路径和曾经出过问题的流程。
八、CI/CD 与预览环境
1. CI 的最小闭环
每个 PR/MR 至少可以执行:
- 使用固定 Node 和包管理器;
- 按锁文件安装依赖;
- 运行 Lint、类型检查和单元测试;
- 构建生产产物并检查体积;
- 执行依赖、许可证和机密扫描;
- 保存测试报告和必要的构建产物。
一个简化的 GitHub Actions 示例:
name: verify
on:
pull_request:
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: <团队批准的固定版本>
- uses: actions/setup-node@v4
with:
node-version: <团队批准的固定版本>
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build
示例中的 Node、pnpm、Action 版本和脚本名都需要按项目实际情况替换、验证;如果仓库没有 typecheck、test 或 build 脚本,应先补齐或改成真实命令。Actions、GitLab CI、Jenkins 等平台都可以实现同样的流程;平台不是重点,重点是检查可重复、权限最小和失败可诊断。
2. 特性分支和预览环境
原文提到的多特性分支环境今天仍然实用:每个 PR 生成一个临时预览地址,产品、设计、测试和后端可以在合并前验收。
实践中要注意:
- 分支名必须经过安全的 slug 化,不能直接拼成域名或命令;
- 预览环境应设置过期清理,避免资源无限增长;
- 使用脱敏数据和最小权限,不要把生产 Token 带入预览环境;
- 前后端接口、数据库和第三方服务要明确绑定关系;
- 预览地址应有访问控制,避免把内部功能公开到互联网;
- 每个预览构建应关联提交 SHA,方便定位和删除。
3. 发布、灰度和回滚
推荐“构建一次,按环境发布”:CI 生成不可变产物,预览、灰度和生产尽量使用同一份产物。运行时配置可以由服务端注入,但不要因此在每个环境重新编译一份不同代码。
发布流程应包含:
- 制品校验和版本记录;
- 预览环境和冒烟检查;
- 分批、灰度或 feature flag;
- 错误率、接口失败率、LCP、INP 和关键转化监控;
- 保留上一份可用版本并能快速回滚;
- 事故后记录原因、影响范围和改进事项。
九、本地开发与团队协作
本地环境应尽量接近 CI,但不必完全复制生产:
- 使用
.env.example说明配置,不提交真实秘密; - 使用 Mock、契约或本地服务减少对共享测试环境的依赖;
- 本地需要 HTTPS 时使用受信任的开发证书,不要关闭浏览器安全校验;
- 对跨域、Cookie、代理和环境变量写成文档;
- README 说明首次安装、常用脚本、故障排查和发布流程。
好的工程化也包括沟通:需求、接口契约、设计稿、缺陷、技术决策和事故记录都应有明确入口。文档可以减少重复沟通,但不能成为拒绝必要讨论的借口。
十、实施顺序建议
第 1 阶段:先让项目可重复
- 固定 Node、包管理器和锁文件;
- 统一
dev/lint/test/build脚本; - 建立 ESLint、格式化和类型检查;
- PR 必须通过 CI;
- 补充 README、环境变量和发布说明。
第 2 阶段:减少重复劳动
- 共享配置和工具包;
- 组件库、设计令牌和 Storybook;
- OpenAPI/JSON Schema 契约;
- 单元测试、关键流程端到端测试;
- 预览环境和依赖安全扫描。
第 3 阶段:提升交付和运营能力
- 构建产物、CDN 和缓存策略;
- 灰度、feature flag 和自动回滚;
- RUM、错误监控和性能预算;
- Monorepo、多包版本和 Changesets;
- 平台模板、脚手架和内部文档站。
每增加一个工具,都要回答三个问题:它解决了哪个真实问题?谁维护?失败时如何回滚?回答不清楚时,先不要引入。