现代 Web 项目技术地图

130 阅读26分钟

现代 Web 应用开发已从简单的页面构建演进为复杂的工程化体系。本文是一份现代 Web 工程化的技术地图,罗列了技术选型、开发环境搭建、代码质量保障、测试体系建设、构建与部署、监控体系建设、业务功能模块、性能优化、文档与协作九个方面涉及的技术。每个技术点以简单叙述的方式介绍,旨在提供一个技术全景的概览。

技术选型

技术选型涉及现代 Web 应用的核心技术栈,包括基础工具链、状态管理、样式方案、视图框架、路由、网络管理等方面。

基础技术栈

  • TypeScript 为 JavaScript 提供静态类型系统,在编译阶段发现潜在错误。类型推导和智能提示提高开发效率,接口定义使团队协作更加清晰。
  • pnpm 通过内容寻址存储(Content-addressable Storage,即相同的包只存储一份,其他项目通过链接引用)和硬链接机制,解决传统包管理器的磁盘空间浪费问题。相比 npm/yarn,可显著节省磁盘空间(具体取决于项目依赖重复度),安装速度提升 2-3 倍。
  • Vite 基于浏览器原生 ES Modules 实现开发服务器,开发模式下直接提供源码的 ESM 模块并按需转换,无需打包。生产构建使用 Rollup,生成优化的产物。

状态管理方案

状态管理方案包括多种选择。

方案特点
Redux单一状态树,严格的单向数据流,DevTools 完善。生态和工具链完整,但样板代码较多
ZustandAPI 简洁,基于 hooks,无需 Provider 包裹。降低使用门槛,适合快速迭代
Jotai原子化状态,按需订阅,细粒度更新(即只有使用该状态的组件会重渲染)。最小化重渲染,适合性能敏感场景

样式方案

样式方案有多种类型。

方案类型说明常见实现
CSS Modules样式局部作用域,避免全局命名冲突Vite/Webpack 内置支持,文件名使用 .module.css 后缀
原子化 CSS预定义原子化 CSS 类,通过组合实现样式Tailwind CSS、UnoCSS、Windi CSS
CSS-in-JS在 JS 中编写样式,运行时或编译时处理styled-components、Emotion、vanilla-extract
CSS 预处理扩展 CSS 语法,提供变量、嵌套、混入等功能Sass、Less、Stylus
CSS 后处理转换和优化 CSS 代码PostCSS
组合 CSS 类名动态组合 CSS 类名的工具库classNames、clsx

具体说明

  • CSS Modules:在 Vite 中,将 CSS 文件命名为 Button.module.css,在组件中导入 import styles from './Button.module.css',使用 className={styles.button} 引用样式。构建时会自动生成唯一类名(如 Button_button_1a2b3c,具体格式取决于构建工具配置),避免全局命名冲突。
  • 原子化 CSS:以 Tailwind CSS 为例,直接在 HTML 中使用预定义类名 <div className="flex items-center gap-4 p-4 bg-white rounded-lg">。UnoCSS 是另一种实现
  • CSS-in-JS:styled-components 和 Emotion 是运行时方案,样式完全动态,类型安全,但有性能开销。vanilla-extract 是编译时方案,提供零运行时开销和类型安全的类型支持
  • CSS 预处理:Sass 提供变量($primary-color)、嵌套、混入(@mixin)、函数等功能。Less 语法与 CSS 更接近,。Stylus 语法灵活。。
  • PostCSS:PostCSS 是 CSS 后处理工具,通过插件系统转换 CSS。常用插件包括 Autoprefixer(自动添加浏览器前缀)、postcss-preset-env(使用最新 CSS 特性并自动转换)、cssnano(压缩 CSS)。PostCSS 可与上述任何方案组合使用。例如你写 backdrop-filter: blur(10px),Autoprefixer 会根据配置的浏览器兼容目标,自动生成 -webkit-backdrop-filter: blur(10px) 等前缀版本。
  • classNames:用于动态组合类名。例如 classNames('btn', { 'btn-active': isActive, 'btn-disabled': isDisabled }),根据条件动态添加或移除类名,避免手动拼接字符串。支持字符串、对象、数组等多种语法,轻量级(不到 1KB),可配合任何 CSS 方案使用。clsx 是 classNames 的性能优化版本,API 完全兼容。

视图框架

视图框架是前端应用的核心,决定组件开发模式。

框架特点适用场景
React虚拟 DOM,单向数据流,生态最丰富。使用 JSX 语法,组件化开发。支持 Hooks,函数式编程友好大型应用,团队协作,需要丰富生态支持
Vue渐进式框架,模板语法,响应式系统。,官方工具链完善(Vue Router、Pinia)中小型应用,快速开发,对模板语法熟悉的团队
Svelte编译时框架,无虚拟 DOM,将组件编译为高效的原生 JavaScript。语法简洁,响应式内置追求极简和高性能的项目,小型到中型应用
Solid细粒度响应式(状态变化时只更新具体受影响的 DOM 节点),无虚拟 DOM,性能优异。类似 React 的 JSX 语法,但响应式更新机制不同性能敏感应用,追求极致性能的项目

工具库

工具库提供常用功能。

通用工具库

  • lodash - 提供数组、对象、字符串等数据操作的实用函数。常用函数包括 _.debounce(防抖)、_.throttle(节流)、_.cloneDeep(深拷贝)、_.get(安全访问嵌套属性)。支持链式调用,可按需引入。
  • ramda - 函数式编程工具库,所有函数自动柯里化。强调数据不可变和函数组合。提供 R.pipe(管道)、R.compose(组合)等函数式编程工具。

日期处理

  • date-fns - 模块化的日期工具库,提供 200+ 日期操作函数。每个函数都是纯函数,支持国际化和时区处理。例如 format(new Date(), 'yyyy-MM-dd')addDays(date, 7)
  • dayjs - 轻量级日期库(仅 2KB),提供日期解析、格式化、计算等功能。支持插件系统扩展功能(如时区、相对时间等)。

数据验证

  • zod - TypeScript 优先的数据验证库。定义 schema 后自动推导类型,无需手动编写 TypeScript 类型。例如 z.object({ name: z.string(), age: z.number() })
  • yup - 基于 schema 的数据验证库。语法简洁,支持同步和异步验证。常与表单库配合使用。

实用工具

  • uuid - 生成符合 RFC4122 标准的唯一标识符(UUID)。支持 v1、v4、v5 等版本。
  • nanoid - 轻量级的唯一 ID 生成器(130 字节)。生成的 ID 更短(默认 21 字符),URL 友好。
  • qs - 查询字符串解析和序列化库。支持嵌套对象、数组等复杂结构。例如 qs.stringify({ a: { b: 'c' } }) 输出 a[b]=c
  • immutable - 构建不可变的数据结构

路由方案

路由管理控制页面导航。

| 方案 | 特点 | | ------------------- | ------------------------------------------------------------------------------ | ---------------------------------------- | | React Router | React 生态最成熟的路由方案,支持嵌套路由、懒加载、路由守卫 | 通用 React 应用,需要成熟稳定的路由方案 | | TanStack Router | 类型安全的路由方案,提供完整的 TypeScript 支持,路由即代码,支持路由级数据加载 | 追求类型安全,需要路由级数据预加载的应用 | | Vue Router | Vue 官方路由,与 Vue 深度集成,支持命名路由、动态路由、导航守卫 | Vue 应用的标准路由方案 |

React Router 示例

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { lazy } from "react";

// 路由级代码分割
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      { path: "dashboard", element: <Dashboard /> },
      { path: "settings", element: <Settings /> },
      { path: "users/:id", element: <UserDetail /> }, // 动态路由
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

网络管理

网络请求和数据同步方案。

| 方案 | 特点 | | ------------------ | -------------------------------------------------------------------- | -------------------------------------- | | Fetch API | 浏览器原生 API,基于 Promise,支持流式读取 | 简单请求场景,不需要额外功能 | | Axios | 功能丰富的 HTTP 客户端,支持请求/响应拦截器、取消请求、自动转换 JSON | 需要拦截器、请求取消等高级功能 | | TanStack Query | 数据同步和缓存方案,提供缓存、重试、轮询、乐观更新等功能 | 复杂数据交互,需要缓存和状态管理的应用 |

开发环境搭建

开发环境涉及 Mock 服务配置、编辑器配置和插件。

Mock 服务方案

Mock 服务用于前后端分离开发。

方案优点缺点适用场景
MSW无需启动独立服务,代码即文档,支持复杂逻辑配置相对复杂单元测试、集成测试、前端开发
JSON Server快速搭建,零配置,RESTful 规范功能简单,不支持复杂逻辑快速原型验证,简单 CRUD 场景

MSW

MSW (Mock Service Worker) 通过 Service Worker 拦截网络请求,在浏览器层面实现 Mock,也可以在测试里使用

// src/mocks/handlers.ts - 定义 Mock 接口
import { http, HttpResponse } from "msw";

export const handlers = [
  // 拦截 GET 请求,支持路径参数
  http.get("/api/user/:id", ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: "John Doe",
      email: "john@example.com",
    });
  }),

  // 拦截 POST 请求,可处理请求体和返回不同状态码
  http.post("/api/login", async ({ request }) => {
    const { username } = await request.json();
    if (username === "admin") {
      return HttpResponse.json({ token: "mock-token" });
    }
    return HttpResponse.json({ error: "Invalid" }, { status: 401 });
  }),
];
// src/main.tsx - 在应用启动时初始化 MSW
if (import.meta.env.DEV) {
  const { setupWorker } = await import("msw/browser");
  const { handlers } = await import("./mocks/handlers");
  const worker = setupWorker(...handlers);
  await worker.start();
}

启动应用后,匹配的网络请求会被 MSW 拦截并返回定义的响应。

JSON Server

JSON Server 通过 JSON 文件快速搭建 REST API。

// db.json - 定义数据结构
{
  "users": [
    { "id": 1, "name": "Alice", "email": "alice@example.com" },
    { "id": 2, "name": "Bob", "email": "bob@example.com" }
  ],
  "posts": [{ "id": 1, "title": "Hello World", "authorId": 1 }]
}
# 启动 JSON Server
npx json-server db.json --port 3001

启动后生成 RESTful 接口:GET /usersGET /users/1POST /usersPUT /users/1DELETE /users/1

编辑器

VSCode 是常用的编辑器。

常用插件

插件功能说明必装程度
ESLint实时检查代码规范,显示错误和警告,支持自动修复必装
Prettier代码格式化工具,统一代码风格必装
Tailwind CSS IntelliSenseTailwind 类名自动补全和语法高亮推荐
Error Lens在代码行内直接显示错误和警告,无需悬停查看推荐
GitLens增强 Git 功能,显示代码作者、提交历史等推荐
Import Cost显示导入包的体积,帮助识别大型依赖推荐
Todo Tree高亮和管理代码中的 TODO、FIXME 等注释推荐
REST Client在 VSCode 中直接测试 HTTP 请求,无需 Postman可选
Thunder Client轻量级 API 测试工具,类似 Postman 的 VSCode 内置版本可选

编辑器配置

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.tabSize": 2,
  "typescript.tsdk": "node_modules/typescript/lib",
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

团队插件配置

// .vscode/extensions.json
{
  "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss"]
}

代码质量保障

代码质量保障涉及工具链和实践方法,包括代码格式化、静态检查、异步规范、异常处理和框架规范。

代码规范工具链

TypeScript/JavaScript 规范工具

工具作用适用场景
Prettier代码格式化工具,统一代码风格(缩进、引号、分号等)强烈推荐用于所有项目,配合编辑器自动格式化
ESLint静态代码分析工具,检查代码质量问题和潜在错误检查逻辑错误、代码规范、最佳实践
oxlintRust 编写的超快 Linter,速度是 ESLint 的 50-100 倍大型项目,追求极致性能
Knip检测未使用的依赖、导出和文件,清理冗余代码定期代码清理,减少项目体积
TypeScript类型检查工具(tsc),编译阶段发现类型错误TypeScript 项目的类型安全保障

其他规范工具

  • Stylelint - CSS/SCSS 代码检查工具,检查样式代码规范(属性顺序、命名规范、兼容性问题等)。配合编辑器可自动修复常见问题。
  • Commitlint - Git 提交信息规范检查工具,强制执行约定式提交(Conventional Commits)格式,如 feat: 添加用户登录功能fix: 修复表单验证错误。配合 husky 在提交前自动检查,确保提交历史清晰可读。

异步规范

现代 Web 应用中充满了异步操作:数据请求、定时器、事件监听、动画等。这些异步操作如果不及时取消,会导致严重问题。

常见场景包括:

  • 组件已卸载但异步请求仍在执行并尝试更新状态,导致内存泄漏
  • 用户快速切换页面时,旧页面的请求覆盖新页面的数据,造成数据错乱
  • 重复发起的请求没有正确取消,浪费资源

为了解决异步取消问题,浏览器提出了标准化的解决方案:AbortSignal。这是浏览器原生 API,无需额外依赖,可用于取消 fetch 请求、事件监听等多种异步操作。使用方式是创建 AbortController 实例,将其 signal 传递给异步操作,需要取消时调用 controller.abort() 即可。

// 使用 AbortController 取消异步请求示例
function UserProfile({ userId }: { userId: string }) {
  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => res.json())
      .then((data) => setUser(data))
      .catch((err) => {
        if (err.name === "AbortError") return; // 忽略取消的请求
        console.error(err);
      });

    // 组件卸载或 userId 变化时自动取消请求
    return () => controller.abort();
  }, [userId]);
}

这种模式确保异步操作的生命周期与组件生命周期同步,避免状态更新错误和内存泄漏。AbortSignal 已成为处理异步取消的标准实践。

异常处理

前端应用运行在用户的浏览器中,网络波动、设备兼容性、第三方库异常等不可控因素都可能导致错误。如果错误未被妥善处理,用户会看到白屏或应用崩溃。完善的异常处理机制能够优雅降级,让应用在出错时依然保持可用,并将错误信息上报便于排查。

异常处理主要包括两个层面:局部错误隔离和全局异常兜底。

局部错误隔离通过错误边界(Error Boundary)模式实现,将应用划分为多个独立区域,某个区域出错时只影响该区域,不会导致整个应用崩溃。主流框架都支持这一模式(React 的 ErrorBoundary、Vue 的 errorCaptured、Svelte 的错误边界等),核心思想是在组件树中捕获子组件的错误并显示降级 UI。

// 错误边界示例(以 React 为例)
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <div>出错了,请刷新页面</div>;
    }
    return this.props.children;
  }
}

全局异常捕获则作为最后的兜底机制,捕获所有未被处理的错误。浏览器提供了两个事件:error 事件捕获同步错误和资源加载错误,unhandledrejection 事件捕获未处理的 Promise 错误。这些错误通常会被上报到监控系统,帮助开发者发现和修复生产环境问题。

// 全局异常捕获
window.addEventListener("error", (event) => {
  console.error("全局错误:", event.error);
  // 上报到监控系统(如 Sentry)
});

window.addEventListener("unhandledrejection", (event) => {
  console.error("未处理的 Promise 错误:", event.reason);
  // 上报到监控系统
});

局部捕获与延迟抛出:在某些场景下,我们希望在局部捕获错误进行收集和记录,但不立即处理,而是在合适的时机统一抛出。这种模式适用于批量操作、数据验证等场景。

// 批量数据处理示例
async function processBatchData(items: Data[]) {
  const errors: Error[] = [];
  const results = [];

  for (const item of items) {
    try {
      const result = await processItem(item);
      results.push(result);
    } catch (error) {
      errors.push(error); // 收集错误,不立即中断
    }
  }

  // 在合适的地方抛出
  if (errors.length > 0) {
    throw new AggregateError(errors, `处理失败:${errors.length} 项`);
  }

  return results;
}

通过局部错误隔离、全局异常捕获和灵活的错误收集策略,可以构建多层防护机制,确保应用的稳定性和可观测性。

框架规范

缺乏统一规范会导致代码风格混乱、文件组织无序、难以定位代码位置。不同开发者各自为政,维护成本急剧上升。建立框架规范的目的是让团队成员遵循统一的约定,降低沟通成本,提高代码可读性和可维护性。

框架规范通常涵盖命名约定、文件组织结构和代码风格三个方面。以下是一种常见的规范方案(以 React/TypeScript 项目为例),实际项目中应根据团队习惯和项目特点调整:

命名约定示例

  • 组件文件使用 PascalCase:UserProfile.tsxLoginButton.tsx
  • Hook 文件使用 camelCase 并以 use 开头:useAuth.tsuseDebounce.ts
  • 工具函数使用 camelCase:formatDate.tsvalidateEmail.ts

文件组织示例

src/
├── components/     # 通用组件
│   ├── Button/
│   │   ├── index.tsx
│   │   ├── Button.module.css
│   │   └── Button.test.tsx
├── features/       # 业务功能模块(按功能划分)
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── api/
├── hooks/          # 通用 Hooks
├── utils/          # 工具函数
└── types/          # TypeScript 类型定义

代码风格建议

  • 组件拆分:单个文件不超过 200-300 行,复杂组件拆分为多个子组件
  • 逻辑复用:重复逻辑提取为自定义 Hooks 或工具函数
  • 类型定义:公共 API 必须有明确的类型定义,避免使用 any
  • 注释原则:解释 Why 而非 What,复杂算法添加注释,避免过度注释

Git Hooks 自动检查

Git Hooks 在 commit 或 push 前自动执行检查。husky + lint-staged 是一种方式

// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["stylelint --fix", "prettier --write"]
  }
}
# .husky/pre-commit
pnpm lint-staged
pnpm type-check

常见检查项:代码格式化、Lint 检查、类型检查、测试运行、提交信息验证(commitlint)。

测试体系建设

测试是保障代码质量和应用稳定性的重要手段。没有测试的项目在重构和迭代时容易引入新 Bug,开发者修改代码时战战兢兢,不敢大胆优化。完善的测试体系能够提供信心保障,让开发者放心修改代码。测试通常分为三个层次:单元测试验证单个函数或模块的逻辑,集成测试验证多个模块协作的正确性,端到端(E2E)测试模拟真实用户操作验证整个应用流程。本章介绍前端测试的常见方案和实践策略。

单元测试和集成测试

单元测试和集成测试,验证代码逻辑和组件行为是否符合预期。测试运行环境分为两种:jsdom(模拟的浏览器环境,速度快但功能有限)和真实浏览器环境(完整的浏览器能力,速度较慢)。大部分功能测试使用 jsdom 即可满足需求,只有涉及复杂浏览器 API(如 Canvas、WebGL)时才需要真实浏览器环境。

以 Vitest 为例,这是一个基于 Vite 的测试框架,启动速度快、配置简单,与 Jest API 兼容。

// utils/math.test.ts - 单元测试示例
import { describe, it, expect } from "vitest";
import { add, multiply } from "./math";

describe("math utils", () => {
  it("add() 应该正确计算两数之和", () => {
    expect(add(1, 2)).toBe(3);
    expect(add(-1, 1)).toBe(0);
  });

  it("multiply() 应该正确计算两数之积", () => {
    expect(multiply(3, 4)).toBe(12);
    expect(multiply(0, 5)).toBe(0);
  });
});

Vitest 支持监听模式(修改代码自动重新运行测试)、覆盖率报告、快照测试等功能。其他主流测试框架如 Jest 也提供类似能力,选择时主要考虑与构建工具的集成度和团队熟悉度。

E2E 测试

E2E(End-to-End)测试模拟真实用户在真实浏览器中的完整操作流程,验证应用的整体功能。与功能测试不同,E2E 测试启动完整的应用服务器,在真实浏览器中打开页面、点击按钮、填写表单、检查结果,覆盖从前端到后端的完整链路。E2E 测试成本较高(速度慢、维护成本高),通常只覆盖核心业务流程。

以 Playwright 为例,这是微软推出的现代 E2E 测试框架,支持 Chromium、Firefox、WebKit 三种浏览器引擎,提供强大的自动等待机制和调试工具。

// e2e/login.spec.ts - E2E 测试示例
import { test, expect } from "@playwright/test";

test("用户登录流程", async ({ page }) => {
  // 访问登录页面
  await page.goto("http://localhost:3000/login");

  // 填写表单
  await page.fill('input[name="username"]', "testuser");
  await page.fill('input[name="password"]', "password123");

  // 点击登录按钮
  await page.click('button[type="submit"]');

  // 验证跳转到首页
  await expect(page).toHaveURL("http://localhost:3000/dashboard");

  // 验证显示用户名
  await expect(page.locator(".username")).toHaveText("testuser");
});

Playwright 的优势在于自动等待(不需要手动 sleep)、支持并行执行测试、提供录制工具(Playwright Codegen)自动生成测试代码。其他主流方案如 Cypress 也很流行,主要区别是 Cypress 运行在浏览器内部而 Playwright 通过自动化协议控制浏览器。

测试策略

测试策略解决"测什么"和"怎么测"的问题。并非所有代码都需要测试,过度测试会增加维护成本。常见的测试策略包括:

测试金字塔原则

  • 大量单元测试(快速、稳定、低成本):测试工具函数、业务逻辑等
  • 适量集成测试(中等速度、中等成本):测试组件交互、API 调用等
  • 少量 E2E 测试(慢速、高成本):只测试核心用户流程

UI 测试方法

UI 测试可以通过多种方式验证:

  1. 行为测试:模拟用户操作,验证 DOM 变化和交互结果(如上面的 Counter 示例)。这是最推荐的方式,关注用户视角而非实现细节。

  2. HTML 快照测试:保存组件渲染的 HTML 结构,后续运行时对比差异。适合验证组件结构不会意外改变。

it("Button 组件快照测试", () => {
  const { container } = render(<Button>Click me</Button>);
  expect(container.firstChild).toMatchSnapshot();
});
  1. 视觉快照测试:截取页面截图与基准图片对比,捕获视觉 Bug。Playwright 内置此功能,但需要在 CI 环境中保证截图一致性。
test("首页视觉快照", async ({ page }) => {
  await page.goto("http://localhost:3000");
  await expect(page).toHaveScreenshot("homepage.png");
});

不同测试方法有不同的适用场景和成本,实际项目中应根据团队资源和项目特点选择合适的测试策略。核心原则是:优先测试高价值代码(核心业务逻辑、复杂算法),避免测试框架代码或过于简单的代码,保持测试可维护性。

构建与部署

构建与部署是将代码转化为可运行应用并交付给用户的关键环节。本章介绍多环境配置管理、自动化 CI/CD 流程、版本管理规范和构建优化策略,帮助团队建立标准化的交付流程,提升部署效率和应用质量。

多环境配置

应用通常需要在多个环境中运行:常用的环境有 开发环境、测试环境、生产环境。开发环境(Development)用于日常开发和调试,测试环境(Staging)用于集成测试和验收,生产环境(Production)面向真实用户。不同环境的配置参数不同(API 地址、数据库连接、第三方服务密钥等),需要统一管理以避免配置错乱。

环境变量管理方案

vite 支持通过 .env 文件管理环境变量:

# .env.development - 开发环境配置
VITE_API_URL=http://localhost:3000/api
VITE_APP_TITLE=My App (Dev)

# .env.production - 生产环境配置
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App

在代码中通过 import.meta.env 访问环境变量:

const apiUrl = import.meta.env.VITE_API_URL;
const appTitle = import.meta.env.VITE_APP_TITLE;

构建工具会根据运行命令(vite devvite build)自动加载对应的 .env 文件。

常见实践

  • .env.local 文件存放本地开发的敏感信息,添加到 .gitignore
  • .env.example 文件作为配置模板提交到仓库
  • 生产环境的敏感配置通过 CI/CD 平台的加密变量管理

CI/CD 流程

CI/CD(持续集成/持续部署)自动化构建、测试和部署流程。主流平台包括 GitHub Actions、GitLab CI、Jenkins 等。

GitHub Actions 示例

GitHub Actions 通过 YAML 文件定义工作流。

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main] # 推送到 main 分支时触发

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4 # 检出代码

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install pnpm
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Run tests
        run: pnpm test # 运行测试

      - name: Build
        run: pnpm build # 构建生产版本

      - name: Deploy to Server
        run: |
          # 部署到服务器(示例:使用 rsync)
          rsync -avz dist/ user@server:/var/www/app

这个工作流会在代码推送到 main 分支时自动执行:安装依赖 → 运行测试 → 构建 → 部署。

版本管理

版本管理规范化软件发布流程。

语义化版本(Semantic Versioning)

语义化版本使用 主版本号.次版本号.修订号 格式(如 1.2.3):

  • 主版本号(Major):不兼容的 API 变更
  • 次版本号(Minor):向后兼容的功能新增
  • 修订号(Patch):向后兼容的问题修复

Changelog 生成

Changelog 记录每个版本的具体变化。常用工具包括:

  • standard-version - 自动更新版本号和生成 Changelog
  • release-please - Google 开源的发布自动化工具

构建优化

构建优化减少应用体积和加载时间。

代码分割(Code Splitting)

代码分割将应用拆分为多个文件,按需加载。

// 路由级代码分割(React Router 示例)
import { lazy } from "react";

const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));

// 用户访问 /dashboard 时才加载 Dashboard 组件的代码

现代打包工具会自动将动态导入的模块拆分为独立文件。

Tree Shaking

Tree Shaking 移除未使用的代码。

// 工具库中定义了很多函数
export function usedFunction() {
  /* ... */
}
export function unusedFunction() {
  /* ... */
} // 这个函数未被使用

// 应用中只引入 usedFunction
import { usedFunction } from "./utils";

// 构建后,unusedFunction 不会被打包到最终产物中

Tree Shaking 要求使用 ES Modules(import/export)。

其他优化手段

  • 压缩与混淆:生产构建压缩 JS/CSS
  • 资源哈希:文件名添加内容哈希(如 main.abc123.js
  • CDN 加速:将静态资源部署到 CDN

监控体系建设

监控体系包括日志系统、错误监控和性能监控等多个维度。

日志系统

日志系统记录应用运行过程中的关键信息,帮助开发者理解应用行为和排查问题。

日志分级

日志通常按严重程度分为多个级别,便于过滤和查找:

  • DEBUG:调试信息,仅开发环境使用,记录详细的执行流程
  • INFO:一般信息,记录正常的业务流程(如用户登录、订单创建)
  • WARN:警告信息,潜在问题但不影响功能(如 API 响应慢、使用了废弃功能)
  • ERROR:错误信息,功能异常但应用仍可运行(如接口调用失败、数据验证错误)
  • FATAL:致命错误,应用无法继续运行(如数据库连接失败)

生产环境通常只记录 INFO 及以上级别,开发环境记录所有级别。

日志收集方案

前端日志收集方式:

  • 浏览器控制台:开发环境输出到 console.logconsole.error
  • 远程日志服务:生产环境将日志发送到日志收集平台
  • 批量上报:累积一定数量或定时上报
// 简单的日志收集示例
class Logger {
  private logs: Array<{ level: string; message: string; timestamp: number }> = [];

  log(level: string, message: string) {
    const log = { level, message, timestamp: Date.now() };
    this.logs.push(log);

    // 开发环境直接输出
    if (import.meta.env.DEV) {
      console[level](message);
    }

    // 累积 10 条或 5 秒后批量上报
    if (this.logs.length >= 10) {
      this.flush();
    }
  }

  flush() {
    if (this.logs.length === 0) return;
    // 发送到日志服务
    fetch("/api/logs", {
      method: "POST",
      body: JSON.stringify(this.logs),
    });
    this.logs = [];
  }
}

日志系统帮助团队追踪业务流程、分析用户行为、定位问题根因。

错误监控

错误监控自动捕获和上报应用中的异常,让开发者及时发现生产环境问题。

Sentry

Sentry 是最流行的错误监控平台,提供错误聚合、堆栈追踪、用户信息、环境信息等功能。集成简单,只需几行代码:

import * as Sentry from "@sentry/browser";

// 初始化 Sentry
Sentry.init({
  dsn: "your-sentry-dsn",
  environment: import.meta.env.MODE,
  release: "1.0.0",
});

// 错误会自动捕获并上报到 Sentry

Sentry 会自动捕获未处理的错误、Promise rejection,并提供详细的错误上下文(浏览器版本、操作系统、用户操作路径等),帮助快速定位和修复问题。

自建错误监控系统

对于有特殊需求或数据安全要求的团队,可以自建错误监控系统。核心功能包括:

  • 错误捕获:监听 window.errorunhandledrejection 事件
  • 错误上报:将错误信息发送到自建服务器
  • 错误分析:按错误类型、发生频率、影响用户数等维度统计

自建系统成本较高,但可以完全控制数据和功能。

错误监控最佳实践

  • 添加 Source Map:生产环境代码经过压缩混淆,Source Map 可将压缩后的堆栈还原为原始代码位置
  • 记录用户操作路径:错误发生前用户做了什么,有助于复现问题
  • 设置错误告警:高频错误或影响大量用户的错误应立即通知团队

性能监控

性能监控追踪应用的加载速度和运行性能,发现性能瓶颈并持续优化。

Web Vitals

Web Vitals 是 Google 提出的核心性能指标,直接影响用户体验和 SEO 排名:

  • LCP (Largest Contentful Paint):最大内容绘制时间,衡量页面主要内容加载速度。目标:< 2.5s
  • INP (Interaction to Next Paint):交互到下一次绘制的延迟,衡量页面整体交互响应速度。目标:< 200ms(Google 已用 INP 替代 FID 作为核心指标)
  • CLS (Cumulative Layout Shift):累积布局偏移,衡量页面视觉稳定性。目标:< 0.1

可以使用 web-vitals 库轻松采集这些指标:

import { onLCP, onINP, onCLS } from "web-vitals";

// 采集并上报性能指标
onLCP((metric) => {
  console.log("LCP:", metric.value);
  // 上报到监控平台
});

onINP((metric) => {
  console.log("INP:", metric.value);
});

onCLS((metric) => {
  console.log("CLS:", metric.value);
});

性能指标采集

除了 Web Vitals,还可以采集其他性能指标:

  • 页面加载时间:通过 performance.timing API 获取各阶段耗时(DNS 查询、TCP 连接、资源加载等)
  • 资源加载性能:通过 performance.getEntriesByType('resource') 获取每个资源的加载时间
  • 运行时性能:使用 performance.mark()performance.measure() 测量代码执行时间

性能监控平台(如 Google Analytics、Datadog RUM)可以聚合这些数据,提供可视化报表和性能趋势分析,帮助团队持续优化应用性能。

性能优化

性能优化提升应用响应速度和加载速度,改善用户体验。本章介绍运行时优化和资源加载优化的常见策略和实现方案。

运行时优化

运行时优化减少不必要的计算和渲染,提升应用响应速度。

虚拟列表

当需要渲染大量数据(如 10000 条记录)时,传统方式会创建 10000 个 DOM 节点,导致页面卡顿。虚拟列表只渲染可见区域的元素,滚动时动态替换内容,大幅减少 DOM 数量。

常用库:

  • react-window - React 虚拟列表库,轻量级(3KB),适合简单场景
  • react-virtualized - 功能更丰富但体积较大,支持复杂表格和网格
  • @tanstack/virtual - 框架无关的虚拟化方案,支持 React、Vue、Solid 等

虚拟列表适用于长列表、表格、聊天记录等场景。

防抖与节流

防抖(Debounce)和节流(Throttle)限制函数执行频率,避免频繁触发导致性能问题。

  • 防抖:连续触发时,只在最后一次触发后执行。适用于搜索输入框(停止输入后才搜索)、窗口 resize(停止调整后才重新布局)
  • 节流:连续触发时,按固定间隔执行。适用于滚动事件(每 100ms 更新一次)、拖拽事件(避免过度计算)
// lodash 提供了现成的实现
import { debounce, throttle } from "lodash";

// 防抖:用户停止输入 300ms 后才搜索
const handleSearch = debounce((keyword) => {
  api.search(keyword);
}, 300);

// 节流:滚动时每 100ms 更新一次
const handleScroll = throttle(() => {
  updateScrollPosition();
}, 100);

memo 优化

React 中的 React.memouseMemo/useCallback 可以避免不必要的重渲染。其他框架(Vue、Solid)也有类似机制。

// React.memo 避免组件不必要的重渲染
const ExpensiveComponent = React.memo(({ data }) => {
  // 只有 data 变化时才重新渲染
  return <div>{/* 复杂渲染逻辑 */}</div>;
});

// useMemo 缓存计算结果
const sortedList = useMemo(() => {
  return data.sort((a, b) => a.value - b.value);
}, [data]); // 只有 data 变化时才重新排序

memo 优化适用于昂贵的计算、复杂的列表渲染等场景。建议根据实际性能瓶颈有针对性地使用,简单组件无需添加 memo。

资源加载优化

资源加载优化减少初始加载时间,提升首屏速度。

图片懒加载

图片懒加载只在图片进入可视区域时才加载,减少初始加载体积。现代浏览器原生支持 loading="lazy" 属性:

<img src="image.jpg" loading="lazy" alt="描述" />

对于复杂场景(如背景图、响应式图片),可使用第三方库如 react-lazy-load-image-component

预加载与预连接

通过 <link> 标签提前加载关键资源或建立连接,缩短加载时间:

<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />

<!-- 预连接到外部域名 -->
<link rel="preconnect" href="https://api.example.com" />

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
  • preload:高优先级加载关键资源(字体、关键 CSS)
  • preconnect:提前建立连接(API 域名、CDN 域名)
  • dns-prefetch:提前解析 DNS(第三方服务)

CDN 加速

CDN(Content Delivery Network,内容分发网络)将静态资源部署到全球各地的节点,用户从最近的节点获取资源,加快访问速度。主流 CDN 服务包括 Cloudflare、阿里云 CDN、腾讯云 CDN。

通常将 JS、CSS、图片、字体等静态资源部署到 CDN,HTML 文件则可以根据实际情况决定是否使用 CDN。

其他优化手段

  • 图片格式优化:使用 WebP/AVIF 格式替代 JPEG/PNG,减少文件体积
  • 响应式图片:使用 <picture> 标签或 srcset 属性,根据设备加载合适尺寸的图片
  • 关键 CSS 内联:将首屏关键 CSS 内联到 HTML 中,减少一次网络请求

文档与协作

文档是知识传承和团队协作的基础。好的文档降低新人上手成本,减少重复沟通,提高开发效率。本章介绍组件文档和开发文档的常见方案和最佳实践。

组件文档

组件文档展示组件的功能、用法和视觉效果,帮助团队成员快速了解和复用组件。

Storybook

Storybook 是最流行的组件文档工具,支持 React、Vue、Angular、Svelte 等多种框架。它为每个组件创建独立的 Story(使用场景),可以交互式地查看和测试组件。

// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

// 默认按钮
export const Primary: Story = {
  args: {
    children: "Click me",
    variant: "primary",
  },
};

// 次要按钮
export const Secondary: Story = {
  args: {
    children: "Click me",
    variant: "secondary",
  },
};

Storybook 提供可视化界面,团队成员可以在浏览器中查看所有组件和变体,调整参数实时预览效果。适合组件库、设计系统的文档化。

开发文档

开发文档帮助团队成员了解项目结构、开发规范和贡献流程。

README

README 是项目的入口文档,通常包含以下内容:

  • 项目简介:项目是什么,解决什么问题
  • 快速开始:如何安装依赖和启动项目
  • 技术栈:使用了哪些技术和工具
  • 项目结构:目录组织和模块划分
  • 常用命令:开发、测试、构建、部署等命令

CONTRIBUTING

CONTRIBUTING 文档说明如何为项目贡献代码,包括:

  • 开发流程:如何创建分支、提交代码、发起 Pull Request
  • 代码规范:命名约定、代码风格、提交信息格式
  • 测试要求:需要编写哪些测试,如何运行测试
  • Review 流程:代码审查的标准和流程

Architecture Decision Records (ADR)

ADR 记录重要的技术决策及其背景和理由。格式通常包括:

  • 标题:简短描述决策内容
  • 状态:提议中、已接受、已废弃等
  • 背景:为什么需要做这个决策
  • 决策:具体采用什么方案
  • 后果:这个决策带来的影响(优点和缺点)

ADR 帮助团队理解历史决策,避免重复讨论,新人可以通过 ADR 快速了解项目的技术演进。

业务功能模块

业务功能模块包括认证鉴权、权限管理、国际化、主题系统、表单处理和数据可视化等。

认证鉴权

认证(Authentication)验证用户身份,鉴权(Authorization)控制用户访问权限。

Session Cookie(传统方案)

Session Cookie 是传统的服务端会话认证方案。用户登录后,服务器创建会话并将会话 ID 存储在 Cookie 中,浏览器在后续请求中自动携带 Cookie。服务器通过会话 ID 查找会话数据,验证用户身份。

Session Cookie 优点是安全性高(使用 httpOnly 属性防止 JavaScript 访问,避免 XSS 攻击),缺点是服务器需要存储会话信息,水平扩展时需要共享会话存储(如 Redis)。

JWT (JSON Web Token)

JWT 是无状态的身份验证方案,服务器签发 Token,客户端在后续请求中携带 Token。服务器验证 Token 有效性,无需存储会话信息。

JWT 由三部分组成:Header(头部)、Payload(负载)、Signature(签名)。服务器用密钥签名,客户端无法伪造。Token 可以存储在 localStorage(简单但有 XSS 风险)或 httpOnly Cookie(更安全)中。

Clerk:Clerk 是一站式用户认证和管理平台,提供开箱即用的认证 UI 组件和完整的用户管理后台。相比自己实现 JWT 或 OAuth,Clerk 简化了整个认证流程,包括注册、登录、密码重置、多因素认证、社交登录等功能。Clerk 适合快速启动项目且不想在认证上投入太多精力的团队,但需要注意它是商业服务,有一定的成本(提供免费额度)。Clerk 的优势:

  • 预制 UI 组件(登录框、注册表单等),无需自己设计
  • 自动处理 Token 刷新、会话管理、安全存储
  • 内置社交登录(Google、GitHub、Twitter 等)
  • 提供用户管理后台,可查看用户列表、封禁用户等
  • 支持组织和多租户功能

权限管理

权限管理控制不同角色用户的访问范围。

路由权限控制

根据用户权限动态生成路由,未授权的路由不可访问。

// React Router 路由权限示例
import { Navigate } from "react-router-dom";

// 路由配置
const routes = [
  { path: "/dashboard", component: Dashboard, roles: ["admin", "editor", "viewer"] },
  { path: "/users", component: Users, roles: ["admin"] },
  { path: "/posts", component: Posts, roles: ["admin", "editor"] },
];

// 受保护路由组件
function ProtectedRoute({ children, allowedRoles }) {
  const user = useCurrentUser();

  if (!user) {
    return <Navigate to="/login" />;
  }

  if (!allowedRoles.includes(user.role)) {
    return <Navigate to="/403" />; // 无权限页面
  }

  return children;
}

// 应用路由配置
function App() {
  return (
    <Routes>
      {routes.map((route) => (
        <Route
          key={route.path}
          path={route.path}
          element={
            <ProtectedRoute allowedRoles={route.roles}>
              <route.component />
            </ProtectedRoute>
          }
        />
      ))}
    </Routes>
  );
}

国际化(i18n)

国际化让应用支持多语言。

react-i18next

// i18n 配置
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

i18n.use(initReactI18next).init({
  resources: {
    en: { translation: { welcome: "Welcome" } },
    zh: { translation: { welcome: "欢迎" } },
  },
  lng: "zh",
});

// 使用
function App() {
  const { t, i18n } = useTranslation();
  return (
    <div>
      <p>{t("welcome")}</p>
      <button onClick={() => i18n.changeLanguage("en")}>English</button>
    </div>
  );
}

主题系统

主题系统支持明暗模式切换或多主题定制。常见方案包括 CSS Variables、Tailwind CSS 暗色模式和 CSS-in-JS 方案。

CSS Variables 方案

使用 CSS 变量定义主题颜色,通过 JavaScript 动态切换。这种方案简单高效,浏览器支持良好,适合传统 CSS 开发流程。

/* 默认(亮色)主题 */
:root {
  --bg-color: #ffffff;
  --text-color: #000000;
}

/* 暗色主题 */
[data-theme="dark"] {
  --bg-color: #000000;
  --text-color: #ffffff;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
}
// 切换主题
function toggleTheme() {
  const theme = document.documentElement.getAttribute("data-theme");
  document.documentElement.setAttribute("data-theme", theme === "dark" ? "light" : "dark");
}

Tailwind CSS 暗色模式

Tailwind 提供内置的暗色模式支持,通过 dark: 前缀定义暗色样式。支持两种策略:类名策略(手动控制)和媒体查询策略(跟随系统)。

// tailwind.config.js
export default {
  darkMode: "class", // 使用类名策略
  // darkMode: 'media', // 使用媒体查询策略(跟随系统)
};
// 使用暗色模式
function Card() {
  return (
    <div className="bg-white dark:bg-gray-800 text-black dark:text-white">
      <h1 className="text-2xl font-bold">标题</h1>
      <p className="text-gray-600 dark:text-gray-300">内容</p>
    </div>
  );
}

// 切换暗色模式
function ThemeToggle() {
  const toggleDarkMode = () => {
    document.documentElement.classList.toggle("dark");
  };
  return <button onClick={toggleDarkMode}>切换主题</button>;
}

表单方案

表单是 Web 应用的核心交互,表单库简化表单状态管理和验证。

方案特点
React Hook Form基于非受控组件,性能优异,API 简洁,与 Zod 等验证库集成
Formik基于受控组件,API 传统,功能完善,生态成熟
Ant Design Form与 Ant Design 深度集成,开箱即用
TanStack Form框架无关,支持 React、Vue、Solid 等

React Hook Form 示例(推荐方案):

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";

// 使用 Zod 定义表单验证 schema
const loginSchema = z.object({
  email: z.string().email("请输入有效的邮箱"),
  password: z.string().min(8, "密码至少 8 位"),
});

type LoginForm = z.infer<typeof loginSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginForm>({
    resolver: zodResolver(loginSchema),
  });

  const onSubmit = async (data: LoginForm) => {
    await api.login(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register("email")} placeholder="邮箱" />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <input {...register("password")} type="password" placeholder="密码" />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "登录中..." : "登录"}
      </button>
    </form>
  );
}