从零搭建现代化复合工程:ESLint、Jest与CI/CD全链路实践
1. 项目概述与核心价值最近在梳理一些工程化项目时发现一个挺有意思的仓库ybbms777/compound-engineering。光看这个名字可能有点抽象——“复合工程”这听起来不像是一个具体的工具或框架更像是一种方法论或者最佳实践的集合。作为一名在软件工程一线摸爬滚打了十多年的老兵我本能地对这类项目产生了兴趣。因为在实际开发中我们常常会遇到这样的困境单个工具或技术栈用起来很顺手但当我们需要把它们组合起来构建一个健壮、可维护、高效协同的现代应用时各种“排异反应”就出现了。配置冲突、依赖管理混乱、构建流程冗长、团队协作规范不一……这些问题消耗的精力往往比实现业务逻辑本身还要多。compound-engineering这个项目其核心价值恰恰在于此。它不是一个从零到一创造新轮子的项目而是一个“工程化解决方案的复合体”。它试图将前端、后端、DevOps、团队规范等领域中那些经过验证的、能良好协同的工具、配置和流程以一种可复用的方式整合在一起。你可以把它理解为一个高度定制化的“项目脚手架生成器”或者一套开箱即用的“企业级工程化规范套件”。它的目标用户非常明确那些厌倦了在每个新项目开始时重复搭建工程化环境、配置各种工具链、制定团队规范的开发团队或技术负责人。对于中小型团队或个人开发者而言从头搭建一套完善的工程化体系成本高昂且容易踩坑。compound-engineering的价值就在于它提供了一个经过实践检验的“样板间”你可以在其基础上进行二次开发快速获得一个包含代码规范、提交规范、自动化测试、CI/CD、依赖管理、监控等能力的现代化项目底座。这不仅能极大提升项目启动效率更能保障项目在初期就具备良好的可维护性和可扩展性基因。2. 核心架构与设计哲学拆解要理解compound-engineering我们不能只把它看作一堆配置文件的堆砌而需要深入其设计哲学和架构思路。根据其命名和常见实践推断这个项目很可能遵循了“约定大于配置”和“模块化复合”两大原则。2.1 “复合”而非“堆砌”的设计理念“复合工程”的关键在于“复合”二字。这意味着它不是简单地把 ESLint、Prettier、Jest、Webpack、Docker、GitHub Actions 等工具扔到一个项目里就完事了。真正的挑战在于让这些工具和谐共处、无缝衔接。工具链的有机整合例如如何让 ESLint 的规则和 Prettier 的代码格式化风格保持一致避免互相冲突如何配置 Husky 的 Git Hooks使得在git commit时自动触发代码检查和单元测试并且只有当检查通过时才允许提交compound-engineering需要提供一套已经调优好的、开箱即用的整合方案。环境与配置的隔离一个现代项目通常需要区分开发、测试、生产等多种环境。compound-engineering需要设计清晰的配置管理策略比如通过.env文件、环境变量注入、或者基于NODE_ENV的条件化配置来确保不同环境下的行为正确且安全。技术栈的适配与抽象项目可能面向 React、Vue、Node.js 等不同技术栈。一个好的复合工程方案其核心的工程化能力如构建、检查、测试应该是与技术栈解耦的或者通过预设的“配方”Preset来轻松适配。这要求架构上具有良好的可插拔性。2.2 典型模块构成推测基于常见的工程化需求我们可以推测compound-engineering项目可能包含以下核心模块代码质量与规范模块静态检查集成 ESLint针对 JavaScript/TypeScript和 Stylelint针对 CSS并预置一套兼顾严格性和实用性的规则集如 Airbnb 规范、Standard 规范的变体。代码格式化集成 Prettier并配置好与 ESLint 协同工作的插件如eslint-config-prettier和eslint-plugin-prettier实现“检查”与“美化”的统一。提交规范集成 Commitizen 和cz-conventional-changelog提供交互式的标准化提交信息生成。同时配合commitlint和 Husky在提交时对信息格式进行校验。操作心得规则集的制定是门艺术。过于宽松形同虚设过于严格则会扼杀开发效率引起团队反感。一个实用的做法是在项目初期采用相对宽松的规则随着团队熟悉度提高再逐步收紧。同时一定要将规则配置文件如.eslintrc.js,.prettierrc纳入版本控制确保团队统一。开发与构建模块核心构建工具根据技术栈可能预置 Webpack 或 Vite 的优化配置。重点可能在于开发服务器的配置HMR热更新、代理设置、生产构建的优化代码分割、压缩、Tree Shaking。语言处理对于 TypeScript 项目需要配置tsconfig.json对于现代 JavaScript需要配置 Babel 或直接利用构建工具自身的转换能力。资源处理预置对图片、字体、CSS 预处理器Sass/Less等资源的处理规则。注意事项构建配置切忌“过度优化”。很多看似高级的配置如极其细粒度的代码分割可能会显著增加构建复杂度而收益在中小型项目中并不明显。遵循“按需优化”原则先解决主要矛盾如打包体积过大再处理次要矛盾。测试与质量保障模块单元测试集成 Jest 或 Vitest配置好测试环境、覆盖率收集istanbul、以及模拟Mock方案。端到端测试可能集成 Cypress 或 Playwright并提供基础的项目结构、测试用例示例和 CI 运行脚本。测试技巧单元测试的重点是“隔离”和“速度”。要善于使用 Jest 的jest.mock来模拟外部依赖。E2E 测试则要注重稳定性和可维护性使用清晰的 Page Object 模式并将测试数据外部化。自动化与部署模块DevOpsGit Hooks通过 Husky 或simple-git-hooks集成在pre-commit阶段运行 lint 和单元测试在commit-msg阶段校验提交信息。持续集成提供 GitHub Actions 或 GitLab CI 的配置文件模板.github/workflows/ci.yml实现代码推送后自动进行 lint、测试和构建。持续部署可能包含基于 Docker 的容器化配置Dockerfile,docker-compose.yml以及部署到常见云平台如 Vercel, Netlify, 或自有服务器的脚本或 CI 配置。避坑指南CI 流水线的失败通知一定要配置好如 Slack、钉钉、企业微信机器人。否则失败的构建可能无人察觉失去其预警价值。另外CI 中运行的测试和构建环境应尽可能与本地和线上环境保持一致避免“在我机器上是好的”这类问题。文档与协作模块项目文档可能集成或推荐使用如 VuePress、Docusaurus 等文档工具并约定文档的存放结构和编写规范。CHANGELOG 生成通过standard-version或conventional-changelog工具结合规范的 Git 提交历史自动生成更新日志。实操心得自动化生成 CHANGELOG 的前提是规范的提交信息。这正是前面引入 Commitizen 和commitlint的意义所在。它能将“编写有意义的提交信息”这一软性要求通过工具固化为硬性流程极大提升项目历史可读性和自动化能力。3. 从零开始实践基于理念搭建你自己的复合工程虽然我们无法直接看到ybbms777/compound-engineering的具体实现但我们可以依据其核心理念动手搭建一个简化但五脏俱全的现代前端复合工程。这里我们以一个React TypeScript Vite技术栈为例。3.1 项目初始化与基础质量保障首先使用 Vite 快速初始化一个项目。npm create vitelatest my-compound-project -- --template react-ts cd my-compound-project npm install接下来安装并配置代码质量工具。# 安装 ESLint 及相关插件 npm install eslint --save-dev npx eslint --init # 交互式选择To check syntax, find problems, and enforce code style # - JavaScript modules (import/export) # - React # - TypeScript: Yes # - Browser # - Use a popular style guide - Airbnb # - Config format: JavaScript # - Yes to install dependencies # 安装 Prettier 及与 ESLint 协同的插件 npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier # 安装 Husky 和 lint-staged用于 Git Hooks npm install --save-dev husky lint-staged npx husky init编辑.eslintrc.cjs集成 Prettier 并做适当调整。module.exports { env: { browser: true, es2020: true }, extends: [ eslint:recommended, plugin:typescript-eslint/recommended, plugin:react-hooks/recommended, airbnb, airbnb/hooks, airbnb-typescript, prettier, // 必须放在最后用于覆盖可能冲突的格式规则 ], parser: typescript-eslint/parser, parserOptions: { ecmaVersion: latest, sourceType: module, project: ./tsconfig.json, // 指定 tsconfig }, plugins: [react-refresh, prettier], rules: { react-refresh/only-export-components: warn, prettier/prettier: error, // 将 Prettier 规则作为 ESLint 错误抛出 react/react-in-jsx-scope: off, // Vite React 17 不需要此规则 import/prefer-default-export: off, // 允许单个命名导出 }, };创建.prettierrc文件定义代码风格。{ semi: true, trailingComma: es5, singleQuote: true, printWidth: 100, tabWidth: 2, endOfLine: auto }编辑package.json添加lint和format脚本并配置lint-staged。{ scripts: { dev: vite, build: tsc vite build, lint: eslint . --ext .js,.jsx,.ts,.tsx --fix, format: prettier --write ., preview: vite preview, prepare: husky install }, lint-staged: { *.{js,jsx,ts,tsx}: [eslint --fix, prettier --write] } }配置 Husky 钩子。编辑.husky/pre-commit文件。#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx lint-staged注意airbnb-typescript配置需要你的tsconfig.json中包含include: [...]。确保你的tsconfig.json或tsconfig.eslint.json正确包含了需要检查的文件。至此我们已经实现了代码提交前的自动检查和格式化。每次执行git commitlint-staged都会对暂存区的文件运行 ESLint自动修复和 Prettier确保进入仓库的代码符合规范。3.2 集成单元测试与提交信息规范安装 Jest 和 React 测试库。npm install --save-dev jest types/jest ts-jest testing-library/react testing-library/jest-dom testing-library/user-event创建jest.config.js。module.exports { preset: ts-jest, testEnvironment: jsdom, setupFilesAfterEnv: [rootDir/jest.setup.js], moduleNameMapper: { ^/(.*)$: rootDir/src/$1, // 如果你使用了路径别名 }, };创建jest.setup.js引入testing-library/jest-dom的扩展。import testing-library/jest-dom;在package.json中添加测试脚本。{ scripts: { test: jest, test:watch: jest --watch, test:coverage: jest --coverage } }现在你可以编写测试了。例如测试src/App.tsx。// src/App.test.tsx import { render, screen } from testing-library/react; import userEvent from testing-library/user-event; import App from ./App; describe(App, () { it(renders hello message, () { render(App /); expect(screen.getByText(/Vite \ React/i)).toBeInTheDocument(); }); it(count increments on button click, async () { const user userEvent.setup(); render(App /); const button screen.getByRole(button, { name: /count is/i }); expect(button).toHaveTextContent(count is 0); await user.click(button); expect(button).toHaveTextContent(count is 1); }); });接下来集成提交信息规范。安装 Commitizen 和相关适配器。npm install --save-dev commitizen cz-conventional-changelog在package.json中配置。{ config: { commitizen: { path: ./node_modules/cz-conventional-changelog } }, scripts: { commit: cz } }安装commitlint来校验提交信息格式。npm install --save-dev commitlint/config-conventional commitlint/cli创建commitlint.config.js。module.exports { extends: [commitlint/config-conventional], };添加 Husky 的commit-msg钩子。运行npx husky add .husky/commit-msg npx --no -- commitlint --edit $1。现在你可以使用npm run commit或git cz来启动交互式提交系统会引导你填写符合 Conventional Commits 规范如feat:,fix:,docs:的提交信息。普通的git commit也会被commitlint校验不符合规范将被拒绝。3.3 配置自动化 CI/CD 流水线我们使用 GitHub Actions 来实现 CI。在项目根目录创建.github/workflows/ci.yml。name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test-and-build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: 18 cache: npm - name: Install dependencies run: npm ci # 使用 ci 而非 install确保依赖锁的一致性 - name: Lint code run: npm run lint - name: Run unit tests run: npm run test - name: Build project run: npm run build # 可选上传构建产物或测试覆盖率报告 # - name: Upload coverage to Codecov # uses: codecov/codecov-actionv3 # with: # files: ./coverage/lcov.info这个工作流会在代码推送到main或develop分支或者创建 Pull Request 时触发。它会依次执行安装依赖、代码检查、单元测试和项目构建。任何一步失败整个工作流就会失败从而阻止有问题的代码被合并。3.4 容器化与生产就绪为了使应用易于部署我们将其容器化。创建Dockerfile。# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build # 生产运行阶段 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]创建nginx.conf以配置 Nginx 处理单页应用的路由。server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }现在你可以通过docker build -t my-app .和docker run -p 8080:80 my-app来构建和运行你的应用镜像。4. 常见问题与排查技巧实录在实际搭建和运行这样一套复合工程体系时你肯定会遇到各种问题。以下是我在实践中总结的一些典型问题及其解决方案。4.1 工具链冲突与配置优先级问题ESLint 和 Prettier 规则冲突导致保存时格式来回变化或者 Prettier 格式化后的代码被 ESLint 报错。排查检查 ESLint 配置中extends数组的顺序。确保prettier或eslint-config-prettier放在最后以便它能够禁用所有可能与 Prettier 冲突的规则。确认已安装eslint-plugin-prettier并在rules中设置了prettier/prettier: error。运行npx eslint --print-config path/to/file.js查看最终生效的 ESLint 规则确认prettier相关规则已正确应用。终极技巧在 VS Code 中可以分别安装 ESLint 和 Prettier 扩展。在项目.vscode/settings.json中设置editor.formatOnSave: true和editor.codeActionsOnSave: { source.fixAll.eslint: true }。但务必确保 Prettier 是作为代码格式化工具而 ESLint 负责修复问题。有时需要明确指定格式化工具editor.defaultFormatter: esbenp.prettier-vscode。4.2 Git Hooks 不生效或执行缓慢问题执行git commit时没有触发pre-commit钩子或者钩子执行时间过长。排查不生效首先确认husky install已执行prepare脚本会在npm install后自动运行。检查.husky/目录下是否有对应的钩子脚本并且脚本有可执行权限chmod x .husky/pre-commit。执行缓慢lint-staged默认会对所有暂存文件运行命令。如果文件很多会很慢。优化lint-staged配置使其只对特定类型的文件运行特定命令并利用缓存。{ lint-staged: { *.{js,jsx,ts,tsx}: [eslint --cache --fix, prettier --write], *.{json,md,css,scss}: [prettier --write] } }eslint --cache会显著提升第二次及以后的检查速度。跳过钩子在极少数需要跳过检查的紧急情况下可以使用git commit --no-verify但这应被视为例外而非常规操作。4.3 CI 流水线环境差异性问题问题代码在本地通过测试但在 CI 环境中失败。排查锁定依赖版本确保使用package-lock.json或yarn.lock并在 CI 中使用npm ci而不是npm install。npm ci会严格根据锁文件安装保证依赖树一致。检查 Node.js 版本在package.json中通过engines字段指定 Node.js 版本范围并在 CI 配置中明确使用相同的版本。{ engines: { node: 16.0.0 19.0.0 } }模拟环境变量本地开发依赖的环境变量如 API 密钥、数据库连接字符串在 CI 中可能不存在。CI 服务通常提供设置 Secrets 的功能如 GitHub Secrets。确保你的代码能优雅地处理缺失的环境变量或者在 CI 脚本中正确设置它们。使用 Docker 镜像如果环境差异非常棘手可以考虑在 CI 中使用自定义的 Docker 镜像该镜像与你的本地开发环境或生产环境高度一致。4.4 测试覆盖率收集与报告问题Jest 覆盖率报告不准确或未包含某些文件。排查配置collectCoverageFrom在jest.config.js中明确指定需要收集覆盖率的文件模式排除node_modules、构建输出目录和测试文件本身。module.exports { // ... 其他配置 collectCoverageFrom: [ src/**/*.{js,jsx,ts,tsx}, !src/**/*.d.ts, !src/**/*.test.{js,jsx,ts,tsx}, !src/**/*.spec.{js,jsx,ts,tsx}, !src/index.tsx, // 通常排除入口文件 ], };检查转换器对于非 JavaScript 文件如 TypeScript、Vue确保配置了正确的转换器如ts-jest。Jest 默认只处理.js文件。行覆盖率与分支覆盖率理解“行覆盖率”Lines和“分支覆盖率”Branches的区别。一个if语句行被覆盖了不代表其true和false两个分支都被覆盖。要关注分支覆盖率它更能反映测试的完备性。4.5 项目膨胀与维护成本问题随着项目发展工程化配置越来越复杂维护成本上升。策略模块化配置不要把所有配置都堆在根目录。将 ESLint、Jest、Webpack/Vite 等配置根据功能或模块拆分到config/目录下然后在主配置文件中引用。例如config/eslint/base.js,config/eslint/react.js。创建预设Preset如果你的团队有多个类似技术栈的项目考虑将通用配置发布为独立的 npm 包如my-org/eslint-config,my-org/jest-config。这样各个项目只需继承这些预设极大简化配置。定期审查与精简每个季度回顾一下你的工程化工具链。有些工具可能已经不再需要例如如果 Vite 内置了某个功能就可以移除对应的插件有些规则的实用性需要重新评估。保持工具链的精简和高效。文档化在项目README.md或专门的DEVELOPMENT.md中清晰地记录工程化套件的使用方式、配置含义、以及如何添加新的工具或规则。这对于新成员上手至关重要。搭建和维护一套像compound-engineering这样的复合工程体系初期确实需要投入不少精力。但它的回报是长期的统一的开发体验、更高的代码质量、自动化的质量关卡、以及可预测的部署流程。它让开发者能更专注于创造业务价值而不是在环境配置和工具调试上浪费时间。当你习惯了这种“武装到牙齿”的工程化开发模式后就很难再回到那种“刀耕火种”的原始状态了。