技术探索与实践解决方案:从零到一重构我的第一个复杂 Web 项目
引言:为什么我要分享这段经历?

作为一名全栈开发工程师,这几年我参与了不少前端和后端的项目。但有一个项目始终让我记忆犹新 —— 那是一次从头开始的技术选型和架构设计,整个过程中充满了挑战、踩坑和成长。我想通过这篇文章,跟你聊聊我是怎么一步步完成这个项目的,也希望通过自己的实战经验,给你一些启发和参考。
这不是一篇泛泛而谈的理论文章,而是一个真实业务场景下的技术决策和落地过程。如果你正在做一个类似的项目,或者也在思考如何构建一个可维护、易扩展的应用系统,我相信这篇文章会对你有帮助。
项目背景

去年,我在一家初创公司负责一个内部管理后台系统的重构工作。原来的系统是基于 Vue 2 + Node.js 搭建的老项目,已经运行了两年多。随着业务增长,老系统的问题逐渐暴露出来:
- 前端结构混乱,没有模块化设计
- API 接口重复请求严重,性能差
- 后端接口响应不稳定,错误处理不统一
- 缺乏测试,改个表单都能把整个页面搞挂
- 团队协作效率低,每次上线都像在走钢丝
于是老板拍板:必须重构!
我们决定采用 React + TypeScript 的方式重写前端,同时后端也进行一定程度的优化升级(主要是服务分层、异常统一处理和中间件优化)。目标很明确:提升开发效率、增强代码可维护性、提高系统稳定性和可扩展性。
遇到的挑战

前端部分
- 状态管理混乱:老系统使用 Vuex,但由于历史原因,很多组件直接操作 state,导致逻辑难以跟踪。
- 组件复用率低:每个功能都是独立写的,UI 组件基本没有复用。
- 接口调用方式不一致:有些地方用了 async/await,有些用了 Promise.then()。
- 样式问题:CSS 管理混乱,经常出现样式冲突或覆盖。
后端部分
- 接口返回格式不统一:有的返回
{ code: 0, data: ... },有的返回{ status: 'success', result: ... }。 - 鉴权机制老旧:还停留在 Cookie+Session,没有使用 JWT 或其他现代方案。
- 日志记录缺失:一旦出错很难定位问题。
- 数据库操作随意:有些 SQL 是直接拼字符串写的,容易出安全问题。
解决方案和技术选型
前端部分
技术栈选择
- 主框架:React 18
- 类型系统:TypeScript
- 状态管理:Redux Toolkit(替代之前的 Redux + Immer 手动管理)
- UI 库:Ant Design + 自己封装的一套通用组件
- 样式方案:CSS Modules + Tailwind CSS(用于快速布局)
架构设计思路
我把整个应用分为以下几个层次:
src/
├── common/ // 公共组件、常量、工具类
├── components/ // 业务组件
├── hooks/ // 自定义 Hook
├── pages/ // 页面组件
├── store/ // 状态管理,使用 RTK
├── services/ // 接口封装
├── routes/ // 路由配置
├── utils/ // 工具函数
└── App.tsx
接口封装策略
为了统一接口调用方式,我做了这样一个封装:
// src/services/index.ts
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 5000,
});
// 添加拦截器统一处理错误
instance.interceptors.response.use(
(res) => {
if (res.data.code === 0) {
return res.data.data;
} else {
console.error('接口错误:', res.data.message);
return Promise.reject(res.data.message);
}
},
(error) => {
console.error('网络错误:', error);
return Promise.reject(error);
}
);
export default instance;
然后每个模块单独封装 service:
// src/services/userService.ts
import request from '../services';
export const fetchUserInfo = () => {
return request.get('/user/info');
};
export const login = (data: { username: string; password: string }) => {
return request.post('/login', data);
};
状态管理优化
使用 Redux Toolkit 之后,状态更新变得特别直观:
// src/store/userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserInfo } from '../services/userService';
interface UserState {
info: any;
loading: boolean;
}
const initialState: UserState = {
info: null,
loading: false,
};
export const getUserInfo = createAsyncThunk('user/fetchUserInfo', async () => {
const res = await fetchUserInfo();
return res;
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getUserInfo.pending, (state) => {
state.loading = true;
})
.addCase(getUserInfo.fulfilled, (state, action) => {
state.loading = false;
state.info = action.payload;
});
},
});
export default userSlice.reducer;
这样不仅提高了代码的可读性和可维护性,也让团队成员更容易上手。
后端部分
技术栈选择
- 主框架:Node.js + Express
- 数据库:MongoDB(原有数据),并逐步引入 MySQL
- ORM:Mongoose + TypeORM(用于新模块)
- 错误处理:自定义中间件统一处理异常
- 日志系统:Winston + Morgan 记录访问日志和错误日志
- 安全认证:JWT 替代 Session
接口统一返回格式
我们约定所有接口返回结构如下:
{
"code": 0,
"message": "成功",
"data": {}
}
其中:
code: 0 表示成功,非 0 表示失败message: 错误信息(成功时为空)data: 返回的数据内容
统一返回格式后,前端处理起来也方便了很多。
错误处理中间件
我们编写了一个全局错误处理器:
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error(`[Error] ${err.stack}`);
const statusCode = err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
code: 1,
message,
data: null,
});
});
这样无论哪个环节出错,都可以统一格式返回给前端。
使用 JWT 替换 Session
我们在登录接口生成 Token:
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET!, {
expiresIn: '1d',
});
并在中间件中校验:
function authenticateJWT(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
try {
const user = jwt.verify(token, process.env.JWT_SECRET!);
req.user = user;
next();
} catch (err) {
return res.status(403).json({ code: 1, message: 'Token无效', data: null });
}
} else {
res.status(401).json({ code: 1, message: '缺少Token', data: null });
}
}
这样一来,前后端就可以轻松实现无状态鉴权。
开发中的“坑”和经验分享
坑1:TypeScript 与 Ant Design 的类型兼容问题
刚开始使用 TypeScript + Ant Design 的时候,我发现某些组件的 props 并没有自动推导出类型,特别是表格的 columns 字段,有时候会出现类型错误。
解决方法:手动为每个 column 编写类型定义,并封装成一个 type 文件统一管理:
type TableColumn<T> = TableProps<T>['columns'];
export const columns: TableColumn<User> = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
// ...
];
坑2:Redux Toolkit 和异步请求配合不好?
其实不是不好,而是我自己一开始没理解清楚。RTK 的 createAsyncThunk 需要结合 extraReducers 使用,不像以前 redux-thunk 那样 dispatch 一个 action 就完了。
后来通过阅读官方文档,才明白应该这样写:
extraReducers: (builder) => {
builder.addCase(fetchData.fulfilled, (state, action) => {
state.data = action.payload;
});
}
建议:一定要多看文档,别自己瞎猜 😅
坑3:JWT 存储位置选择不当导致的安全隐患
最开始我将 Token 存在 localStorage 里,结果某天发现浏览器控制台可以轻易访问修改 Token,存在 XSS 漏洞风险。
改进方案:
- 使用 HttpOnly Cookie 存储 Token(更安全)
- 配合 CSRF Token 防止跨站攻击
虽然前端取不到 Token,但我们可以通过封装 Axios 实现自动携带 Token:
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
});
最终效果和收益
经过大约两个半月的努力,我们完成了整个系统的重构。最终上线后,系统稳定性明显提升,具体表现如下:
| 指标 | 旧系统 | 新系统 | 提升幅度 |
|---|---|---|---|
| 首屏加载速度 | ~3s | ~1.2s | 60%↑ |
| 代码重复率 | >40% | <10% | 减少75% |
| 上线故障次数/月 | 5~8次 | 0~1次 | 显著减少 |
| 团队协作效率 | 较低 | 中等↑ | 提升明显 |
除了技术指标外,团队协作体验也好了不少。新人加入后,有了清晰的目录结构和统一编码规范,上手时间大大缩短。
写在最后:几点经验总结
- 技术选型要根据团队能力和实际业务需求来定:不要盲目追求新技术,适合的才是最好的。
- 统一规范比工具更重要:无论你是用 Redux 还是 Zustand,关键是形成一套大家都遵守的开发规范。
- 尽早引入测试机制:我们这次重构前期没写测试,后期补起来特别痛苦。现在我会坚持单元测试 + E2E 测试双管齐下。
- 持续集成/部署要同步做:我们是在开发完一半才接入 CI/CD,结果返工了不少。早点搞,真香!
- 多写文档,哪怕是简单的 README:它真的能帮你省下很多沟通成本。
- 重构是个持久战,别想着一口吃成胖子:可以分阶段来做,比如先拆解模块,再逐步替换核心逻辑。
结语
这一路走来,确实不容易。但也正是这些“掉进坑里又爬出来”的经历,让我对全栈开发有了更深的理解。希望这篇来自一线实战的文章,能给你带来一些启发和帮助。
如果你也在做类似的重构工作,欢迎留言交流,说不定咱们能一起探讨更好的做法。记住,技术这条路从来都不是一个人在战斗 🤚
祝你码出优雅,写出健壮的代码。

评论 0