第七篇:【React 实战项目】从零构建企业级应用完全指南

手把手打造专业级 React 应用,产品经理再也挑不出毛病!

嘿,React 开发者们!经过前面六篇的系统学习,你已经掌握了 React 开发的核心知识和技巧。但理论终归是理论,今天我们要将所有这些知识点融会贯通,打造一个真正企业级的 React 应用!

让我们从零开始,一步步构建一个功能完整、架构优雅的项目管理系统,这将是你简历上的亮点项目!

1. 项目架构与技术选型

首先,让我们确定项目的技术栈和架构:

bash 复制代码
# 项目结构
project-management-system/
├── public/                 # 静态资源
├── src/
│   ├── assets/            # 图片、字体等资源
│   ├── components/        # 通用UI组件
│   │   ├── common/        # 基础组件
│   │   ├── layout/        # 布局组件
│   │   └── features/      # 业务组件
│   ├── config/            # 配置文件
│   ├── hooks/             # 自定义钩子
│   ├── pages/             # 页面组件
│   ├── services/          # API服务
│   ├── stores/            # 状态管理
│   ├── types/             # TypeScript类型
│   ├── utils/             # 工具函数
│   ├── App.tsx            # 应用入口
│   ├── index.tsx          # 渲染入口
│   └── routes.tsx         # 路由配置
├── .env.development       # 开发环境变量
├── .env.production        # 生产环境变量
├── .eslintrc.js           # ESLint配置
├── jest.config.js         # 测试配置
├── package.json           # 依赖管理
├── tsconfig.json          # TypeScript配置
└── vite.config.ts         # Vite配置

技术选型

jsx 复制代码
// package.json 主要依赖
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.14.0",        // 路由管理
    "@tanstack/react-query": "^4.29.15",  // 服务端状态管理
    "zustand": "^4.3.8",                  // 客户端状态管理
    "axios": "^1.4.0",                    // HTTP请求
    "react-hook-form": "^7.45.0",         // 表单管理
    "zod": "^3.21.4",                     // 数据验证
    "dayjs": "^1.11.8",                   // 日期处理
    "antd": "^5.6.3",                     // UI组件库
    "styled-components": "^6.0.0",        // CSS-in-JS
    "react-error-boundary": "^4.0.10",    // 错误边界
    "i18next": "^23.2.0",                 // 国际化
    "react-i18next": "^13.0.0"            // React国际化
  },
  "devDependencies": {
    "typescript": "^5.1.3",               // TypeScript
    "vite": "^4.3.9",                     // 构建工具
    "vitest": "^0.32.2",                  // 单元测试
    "cypress": "^12.16.0",                // E2E测试
    "@testing-library/react": "^14.0.0",  // 组件测试
    "eslint": "^8.43.0",                  // 代码检查
    "prettier": "^2.8.8"                  // 代码格式化
  }
}

2. 认证与权限系统实现

所有企业级应用的第一步是构建完善的认证与权限系统:

tsx 复制代码
// src/stores/authStore.ts - Zustand认证状态管理
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { loginApi, logoutApi, refreshTokenApi } from "../services/authService";

interface AuthState {
  user: User | null;
  token: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  permissions: string[];
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => Promise<void>;
  refreshAccessToken: () => Promise<string>;
  hasPermission: (permission: string) => boolean;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      refreshToken: null,
      isAuthenticated: false,
      permissions: [],

      async login(credentials) {
        const { user, token, refreshToken, permissions } = await loginApi(
          credentials
        );
        set({
          user,
          token,
          refreshToken,
          permissions,
          isAuthenticated: true,
        });
      },

      async logout() {
        await logoutApi();
        set({
          user: null,
          token: null,
          refreshToken: null,
          permissions: [],
          isAuthenticated: false,
        });
      },

      async refreshAccessToken() {
        const { refreshToken } = get();
        if (!refreshToken) throw new Error("No refresh token");

        const { token: newToken } = await refreshTokenApi(refreshToken);
        set({ token: newToken });
        return newToken;
      },

      hasPermission(permission) {
        return get().permissions.includes(permission);
      },
    }),
    {
      name: "auth-storage",
      partialize: (state) => ({
        token: state.token,
        refreshToken: state.refreshToken,
        user: state.user,
        permissions: state.permissions,
      }),
    }
  )
);
tsx 复制代码
// src/components/common/ProtectedRoute.tsx - 权限控制路由
import { Navigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../../stores/authStore";

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredPermission?: string;
}

export function ProtectedRoute({
  children,
  requiredPermission,
}: ProtectedRouteProps) {
  const { isAuthenticated, hasPermission } = useAuthStore();
  const location = useLocation();

  if (!isAuthenticated) {
    // 重定向到登录页,保留原始访问路径
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // 检查是否有必要的权限
  if (requiredPermission && !hasPermission(requiredPermission)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <>{children}</>;
}
tsx 复制代码
// src/services/axiosInstance.ts - 请求拦截器与认证令牌刷新
import axios from "axios";
import { useAuthStore } from "../stores/authStore";

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

// 请求拦截器添加认证令牌
apiClient.interceptors.request.use(
  (config) => {
    const token = useAuthStore.getState().token;
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器处理认证失败
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // 如果是401错误且不是刷新token的请求,尝试刷新令牌
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const newToken = await useAuthStore.getState().refreshAccessToken();
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // 刷新令牌失败,登出用户
        await useAuthStore.getState().logout();
        window.location.href = "/login";
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

3. 数据获取与 React Query 集成

高效的数据获取是企业级应用的关键:

tsx 复制代码
// src/services/projectService.ts - API服务封装
import apiClient from "./axiosInstance";
import { Project, CreateProjectDto, UpdateProjectDto } from "../types";

export const projectService = {
  async getAll(): Promise<Project[]> {
    const { data } = await apiClient.get("/projects");
    return data;
  },

  async getById(id: string): Promise<Project> {
    const { data } = await apiClient.get(`/projects/${id}`);
    return data;
  },

  async create(project: CreateProjectDto): Promise<Project> {
    const { data } = await apiClient.post("/projects", project);
    return data;
  },

  async update(id: string, project: UpdateProjectDto): Promise<Project> {
    const { data } = await apiClient.put(`/projects/${id}`, project);
    return data;
  },

  async delete(id: string): Promise<void> {
    await apiClient.delete(`/projects/${id}`);
  },
};
tsx 复制代码
// src/hooks/useProjects.ts - React Query钩子封装
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { projectService } from "../services/projectService";
import { Project, CreateProjectDto, UpdateProjectDto } from "../types";

// 获取项目列表
export function useProjects() {
  return useQuery({
    queryKey: ["projects"],
    queryFn: projectService.getAll,
    staleTime: 5 * 60 * 1000, // 5分钟缓存
  });
}

// 获取单个项目
export function useProject(id: string) {
  return useQuery({
    queryKey: ["projects", id],
    queryFn: () => projectService.getById(id),
    enabled: !!id, // 只有id存在时才请求
  });
}

// 创建项目
export function useCreateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newProject: CreateProjectDto) =>
      projectService.create(newProject),
    onSuccess: (data) => {
      // 创建成功后更新项目列表缓存
      queryClient.setQueryData<Project[]>(["projects"], (old = []) => [
        ...old,
        data,
      ]);
      queryClient.invalidateQueries({ queryKey: ["projects"] });
    },
  });
}

// 更新项目
export function useUpdateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateProjectDto }) =>
      projectService.update(id, data),
    onSuccess: (updatedProject) => {
      // 更新项目缓存
      queryClient.setQueryData<Project>(
        ["projects", updatedProject.id],
        updatedProject
      );

      // 更新项目列表中的项目
      queryClient.setQueryData<Project[]>(["projects"], (old = []) =>
        old.map((project) =>
          project.id === updatedProject.id ? updatedProject : project
        )
      );
    },
  });
}

// 删除项目
export function useDeleteProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => projectService.delete(id),
    onSuccess: (_, id) => {
      // 从缓存中移除项目
      queryClient.removeQueries({ queryKey: ["projects", id] });

      // 更新项目列表
      queryClient.setQueryData<Project[]>(["projects"], (old = []) =>
        old.filter((project) => project.id !== id)
      );
    },
  });
}

4. 表单处理与数据验证

企业应用中表单处理至关重要,让我们实现一个完善的表单系统:

tsx 复制代码
// src/components/features/ProjectForm.tsx - 项目表单组件
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, Form, Input, DatePicker, Select, message } from "antd";
import { useCreateProject, useUpdateProject } from "../../hooks/useProjects";
import { Project } from "../../types";

// 使用Zod定义表单验证模式
const projectSchema = z
  .object({
    name: z.string().min(3, "项目名至少3个字符").max(100),
    description: z.string().optional(),
    startDate: z.date({
      required_error: "请选择开始日期",
    }),
    endDate: z
      .date({
        required_error: "请选择结束日期",
      })
      .optional(),
    status: z.enum(["planning", "active", "completed", "on-hold"]),
    priority: z.enum(["low", "medium", "high", "urgent"]),
  })
  .refine((data) => !data.endDate || data.startDate <= data.endDate, {
    message: "结束日期必须晚于开始日期",
    path: ["endDate"],
  });

// 表单数据类型
type ProjectFormData = z.infer<typeof projectSchema>;

interface ProjectFormProps {
  project?: Project;
  onSuccess?: () => void;
}

export function ProjectForm({ project, onSuccess }: ProjectFormProps) {
  // 表单处理
  const {
    control,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<ProjectFormData>({
    resolver: zodResolver(projectSchema),
    defaultValues: project
      ? {
          ...project,
          startDate: new Date(project.startDate),
          endDate: project.endDate ? new Date(project.endDate) : undefined,
        }
      : {
          status: "planning",
          priority: "medium",
          startDate: new Date(),
        },
  });

  // API调用钩子
  const createMutation = useCreateProject();
  const updateMutation = useUpdateProject();

  // 表单提交处理
  const onSubmit = async (data: ProjectFormData) => {
    try {
      if (project) {
        // 更新现有项目
        await updateMutation.mutateAsync({
          id: project.id,
          data,
        });
        message.success("项目已更新");
      } else {
        // 创建新项目
        await createMutation.mutateAsync(data);
        message.success("项目已创建");
        reset(); // 清空表单
      }

      onSuccess?.();
    } catch (error) {
      message.error("操作失败,请重试");
      console.error(error);
    }
  };

  const isSubmitting = createMutation.isPending || updateMutation.isPending;

  return (
    <Form layout="vertical" onFinish={handleSubmit(onSubmit)}>
      <Form.Item
        label="项目名称"
        validateStatus={errors.name ? "error" : undefined}
        help={errors.name?.message}
      >
        <Controller
          name="name"
          control={control}
          render={({ field }) => <Input {...field} disabled={isSubmitting} />}
        />
      </Form.Item>

      <Form.Item label="项目描述">
        <Controller
          name="description"
          control={control}
          render={({ field }) => (
            <Input.TextArea {...field} rows={4} disabled={isSubmitting} />
          )}
        />
      </Form.Item>

      <div style={{ display: "flex", gap: 16 }}>
        <Form.Item
          label="开始日期"
          validateStatus={errors.startDate ? "error" : undefined}
          help={errors.startDate?.message}
          style={{ flex: 1 }}
        >
          <Controller
            name="startDate"
            control={control}
            render={({ field: { value, onChange } }) => (
              <DatePicker
                value={value ? dayjs(value) : null}
                onChange={(date) => onChange(date?.toDate())}
                style={{ width: "100%" }}
                disabled={isSubmitting}
              />
            )}
          />
        </Form.Item>

        <Form.Item
          label="结束日期"
          validateStatus={errors.endDate ? "error" : undefined}
          help={errors.endDate?.message}
          style={{ flex: 1 }}
        >
          <Controller
            name="endDate"
            control={control}
            render={({ field: { value, onChange } }) => (
              <DatePicker
                value={value ? dayjs(value) : null}
                onChange={(date) => onChange(date?.toDate())}
                style={{ width: "100%" }}
                disabled={isSubmitting}
              />
            )}
          />
        </Form.Item>
      </div>

      <div style={{ display: "flex", gap: 16 }}>
        <Form.Item label="状态" style={{ flex: 1 }}>
          <Controller
            name="status"
            control={control}
            render={({ field }) => (
              <Select {...field} disabled={isSubmitting}>
                <Select.Option value="planning">规划中</Select.Option>
                <Select.Option value="active">进行中</Select.Option>
                <Select.Option value="completed">已完成</Select.Option>
                <Select.Option value="on-hold">已搁置</Select.Option>
              </Select>
            )}
          />
        </Form.Item>

        <Form.Item label="优先级" style={{ flex: 1 }}>
          <Controller
            name="priority"
            control={control}
            render={({ field }) => (
              <Select {...field} disabled={isSubmitting}>
                <Select.Option value="low">低</Select.Option>
                <Select.Option value="medium">中</Select.Option>
                <Select.Option value="high">高</Select.Option>
                <Select.Option value="urgent">紧急</Select.Option>
              </Select>
            )}
          />
        </Form.Item>
      </div>

      <Form.Item>
        <Button type="primary" htmlType="submit" loading={isSubmitting} block>
          {project ? "更新项目" : "创建项目"}
        </Button>
      </Form.Item>
    </Form>
  );
}

5. 国际化与主题管理

企业级应用常需要支持多语言和主题切换:

tsx 复制代码
// src/config/i18n.ts - 国际化配置
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

// 导入翻译文件
import enTranslation from "../locales/en.json";
import zhTranslation from "../locales/zh.json";

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: {
        translation: enTranslation,
      },
      zh: {
        translation: zhTranslation,
      },
    },
    fallbackLng: "en",
    interpolation: {
      escapeValue: false, // React已处理XSS
    },
  });

export default i18n;
tsx 复制代码
// src/hooks/useTheme.ts - 主题管理Hook
import { create } from "zustand";
import { persist } from "zustand/middleware";

type ThemeMode = "light" | "dark" | "system";

interface ThemeState {
  mode: ThemeMode;
  setMode: (mode: ThemeMode) => void;
  isDarkMode: boolean;
}

export const useThemeStore = create<ThemeState>()(
  persist(
    (set, get) => ({
      mode: "system",

      setMode: (mode) => set({ mode }),

      get isDarkMode() {
        const { mode } = get();
        if (mode === "system") {
          return window.matchMedia("(prefers-color-scheme: dark)").matches;
        }
        return mode === "dark";
      },
    }),
    {
      name: "theme-storage",
    }
  )
);

// 主题应用Hook
export function useTheme() {
  const { mode, setMode, isDarkMode } = useThemeStore();

  // 监听系统主题变化
  useEffect(() => {
    if (mode !== "system") return;

    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const handleChange = () => {
      // 强制组件重新渲染
      setMode("system");
    };

    mediaQuery.addEventListener("change", handleChange);
    return () => mediaQuery.removeEventListener("change", handleChange);
  }, [mode, setMode]);

  // 应用主题到文档
  useEffect(() => {
    const root = window.document.documentElement;
    root.classList.remove("light-theme", "dark-theme");
    root.classList.add(isDarkMode ? "dark-theme" : "light-theme");
  }, [isDarkMode]);

  return { mode, setMode, isDarkMode };
}

下一篇预告:《【React 性能调优】从优化实践到自动化性能监控》

在系列的下一篇中,我们将深入探讨如何对 React 应用进行全方位的性能优化:

  • React 开发者工具与性能分析
  • 代码分割与懒加载进阶技巧
  • 服务端渲染(SSR)与静态生成(SSG)
  • 自动化性能监控方案
  • 实际项目优化案例分析

优秀的 React 应用不只是功能完善,更要体验流畅。下一篇,我们将带你的应用性能再上一个台阶!

敬请期待!

关于作者

Hi,我是 hyy,一位热爱技术的全栈开发者:

  • 🚀 专注 TypeScript 全栈开发,偏前端技术栈
  • 💼 多元工作背景(跨国企业、技术外包、创业公司)
  • 📝 掘金活跃技术作者
  • 🎵 电子音乐爱好者
  • 🎮 游戏玩家
  • 💻 技术分享达人

加入我们

欢迎加入前端技术交流圈,与 10000+开发者一起:

  • 探讨前端最新技术趋势
  • 解决开发难题
  • 分享职场经验
  • 获取优质学习资源

添加方式:掘金摸鱼沸点 👈 扫码进群

相关推荐
—Qeyser5 小时前
用 Deepseek 写的uniapp血型遗传查询工具
前端·javascript·ai·chatgpt·uni-app·deepseek
codingandsleeping5 小时前
HTTP1.0、1.1、2.0 的区别
前端·网络协议·http
小满blue5 小时前
uniapp实现目录树效果,异步加载数据
前端·uni-app
喜樂的CC7 小时前
[react]Next.js之自适应布局和高清屏幕适配解决方案
javascript·react.js·postcss
天天扭码7 小时前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
咖啡虫7 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
烛阴8 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
拉不动的猪8 小时前
设计模式之------策略模式
前端·javascript·面试
旭久8 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc8 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf