手把手打造专业级 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+开发者一起:
- 探讨前端最新技术趋势
- 解决开发难题
- 分享职场经验
- 获取优质学习资源
添加方式:掘金摸鱼沸点 👈 扫码进群