在 Vibe Coding 浪潮席卷而来的今天,AI 辅助开发已经不再是新鲜事。笔者所在团队维护着一个内部业务系统(技术栈:React 18 + Vite + React Router),前端独立部署,后端由 Java 同学负责。这套架构运行了两年多,一直相安无事。
直到有一天,组织架构调整,后端同学被调去支援其他项目(AI创新项目),老板拍板:前端同学顶上后端的活儿。 好家伙,说得轻巧,前后端代码都不在一个仓库,让前端同学怎么顶?
现状分析:前后端分离之痛
先来看看原来的项目结构:
前端仓库 (frontend-repo)
├── src/
│ ├── pages/
│ ├── components/
│ └── utils/
└── package.json
后端仓库 (backend-repo)
├── src/main/java/
│ ├── controller/
│ ├── service/
│ └── mapper/
└── pom.xml
看起来很标准对吧?但问题来了:
痛点一:AI 辅助开发的先天不足
用过 Cursor、CodeBuddy 这类 AI 编程工具的同学都知道,AI 需要理解上下文才能给出靠谱的建议。当你让 AI 帮你实现一个完整功能时,它需要同时看到:
- 前端的组件结构和 API 调用
- 后端的接口定义和业务逻辑
- 数据库的表结构
但是,前后端分离的架构下,AI 只能看到半边天。让它帮你写个表单提交功能,它只能帮你写前端调用,后端接口得你自己跑到另一个仓库里去补。这就像让一个人蒙着一只眼睛打乒乓球——不是不能打,就是费劲。
痛点二:前端同学的上手成本
前端同学接手后端代码,第一反应是:这 Spring Boot 的注解也太多了吧?@RestController、@Autowired、@Transactional... 光是理解这些就得花不少时间。
更要命的是,本地调试还得:
- 先启动 MySQL
- 再启动 Redis
- 配置一堆环境变量
- 最后启动 Spring Boot
前端同学看到这套流程,内心 OS:我就改个接口返回值,至于吗?
痛点三:联调效率低下
前后端分离开发时,联调是个老大难问题:
- 前端:接口好了吗?
- 后端:好了,你试试
- 前端:报错了,返回格式不对
- 后端:我看看... 改好了
- 前端:还是不行,字段名不一致
- (循环往复...)
来回切换仓库、对着接口文档核对字段,这种低效的协作模式在 AI 时代显得尤为刺眼。
破局:全栈架构升级
经过一番调研,笔者决定将项目升级为 Express + React + Vite 的全栈架构。为什么选这套?
- Express:轻量、灵活,前端同学学习成本低,写 JavaScript 就能搞后端
- TypeScript 全栈:前后端共享类型定义,编译期就能发现问题
- Vite:开发体验一流,HMR 快得飞起
- 单一仓库:AI 终于能看到全貌了
最终的项目结构长这样:
fullstack-web-app/
├── client/ # 前端代码
│ ├── pages/ # 页面组件
│ ├── components/ # 可复用组件
│ ├── hooks/ # React Hooks
│ ├── utils/ # 工具函数
│ ├── App.tsx # 根组件
│ └── main.tsx # 前端入口
│
├── server/ # 后端代码
│ ├── middleware/ # Express 中间件
│ ├── utils/ # 工具函数
│ └── server.ts # 服务端入口
│
├── env.ts # 环境变量
├── package.json # 统一依赖管理
└── tsconfig.json # TypeScript 配置
一眼望去,前端后端都在这儿了,AI 表示很满意。更重要的是前端写 nodejs 天然无障碍!
技术选型详解
一、后端框架:Express
为什么不用 NestJS 或者 Koa?
NestJS 功能确实强大,但那套装饰器和依赖注入的玩法,跟 Spring Boot 有异曲同工之妙。前端同学刚从 Java 的"注解地狱"逃出来,别又给整进去了。
Koa 挺好,但生态不如 Express 丰富。选 Express 就图一个:中间件多、文档全、前端同学一看就懂。
服务端入口 server.ts 的核心结构:
import express from "express";
import "express-async-errors";
export async function startup() {
const app = express();
// HTTP 日志(仅 API)
app.use("/api", serveHttpLogger());
// API 路由
app.use("/api", serveApi());
// 静态资源服务
if (isProd) {
app.use("/assets", serveAssets());
}
// 前端路由
if (isProd || isDebug) {
app.use(serveIndex());
} else {
// 开发模式:集成 Vite
app.use("/", await serveClientVite());
}
// 全局错误处理
app.use(serveErrorHandler());
app.listen(port, () => {
logger.info(`Server running on port ${port}`);
});
}
express-async-errors 这个库必须夸一下,有了它,async/await 里的错误会自动被全局错误处理中间件捕获,再也不用写一堆 try-catch 了。
二、本地开发与热更新
开发体验是生产力的关键。这套架构的开发模式是这样的:
{
"scripts": {
"dev": "cross-env NODE_ENV=local tsx watch --inspect=9442 server/server.ts"
}
}
一条命令启动,背后做了这些事:
- tsx watch:监听 TypeScript 文件变化,服务端代码改了自动重启
- Vite Dev Server:前端代码改了,浏览器自动热更新(不刷新页面)
- 统一端口:前后端都走 3003 端口,不用配代理
Vite 的集成是通过中间件实现的:
import { createServer, createViteRuntime } from "vite";
export async function serveClientVite() {
const vite = await createServer({
configFile: resolve(__dirname, "../client/vite.config.ts"),
server: { middlewareMode: true },
appType: "custom",
});
const router = Router();
// Vite 中间件处理前端资源
router.use(vite.middlewares);
// 所有非 API 请求返回 index.html(SPA 路由支持)
router.use("*", async (req, res, next) => {
const url = req.originalUrl;
let template = fs.readFileSync(
resolve(__dirname, "../client/index-dev.html"),
"utf-8"
);
template = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(template);
});
return router;
}
这套方案的好处是:
- 前端同学还是熟悉的 Vite 开发体验
- 不需要额外配置跨域代理
- API 和页面请求走同一个端口,调试方便
三、环境隔离
环境管理是个容易被忽视但很重要的环节。笔者设计了三种环境:
| 环境 | NODE_ENV | 特点 |
|---|---|---|
| 本地开发 | local | Vite Dev Server,完整 HMR |
| 联调测试 | development | 使用构建后的前端资源 |
| 生产环境 | production | 静态资源 + API 服务 |
环境变量管理使用 dotenv,并且在启动时强制校验必需变量:
// env.ts
import "dotenv/config";
export const { NODE_ENV, PORT, DATA_DIR } = process.env;
export const DEV = NODE_ENV === "development";
export const LOCAL = NODE_ENV === "local";
// 启动校验
for (const [key, value] of Object.entries({ NODE_ENV, DATA_DIR })) {
if (!value) {
throw new Error(`请设置 ${key} 环境变量`);
}
}
少了哪个环境变量,启动就报错,避免线上出问题了才发现配置没写。
四、构建流程
构建分两步:
1. 前端构建
npm run build:client
Vite 会把前端代码打包到 client/dist 目录,资源文件名带 hash,方便 CDN 缓存。
这里有个小细节,Vite 默认的 hash 算法生成的文件名可能包含 -,部分 CDN 对此支持不好。所以我自定义了 hash 算法:
// vite.config.ts
function customMd5HashAlgorithm(data: Buffer): string {
// 只使用十六进制字符,兼容 CDN
return createHash("md5").update(data).digest("hex").slice(0, 8);
}
export default defineConfig({
build: {
rollupOptions: {
output: {
hashCharacters: customMd5HashAlgorithm,
},
},
},
});
2. 后端部署
后端代码不需要编译,直接用 tsx 运行 TypeScript。生产环境启动命令:
npm start
五、服务日志
日志系统使用 Winston + DailyRotateFile:
const logger = winston.createLogger({
level: LOG_LEVEL,
format: winston.format.combine(
winston.format.timestamp({
format: () => dayjs().format("YYYY-MM-DD HH:mm:ss.SSS"),
}),
winston.format.json()
),
transports: [
// 控制台输出(带颜色)
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
// 文件输出(按小时轮转)
new DailyRotateFile({
dirname: LOG_DIR,
filename: "app-%DATE%.log",
datePattern: "YYYY-MM-DD-HH",
maxSize: "100m",
maxFiles: "7d",
}),
],
});
HTTP 请求日志也做了定制,记录请求耗时、响应大小等关键信息:
// 日志格式示例
{
"timestamp": "2026-03-19 14:30:25.123",
"level": "info",
"method": "POST",
"url": "/api/submit",
"status": 200,
"duration": "45ms",
"responseSize": "1.2KB"
}
六、Docker 部署
项目提供了 Dockerfile,一键部署:
FROM node:20-slim
# 时区设置
RUN rm -f /etc/localtime \
&& ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /app
COPY . ./
ENV DATA_DIR=/app/data
RUN npm install --force --registry=https://registry.npmmirror.com
EXPOSE 3003
ENTRYPOINT ["npm", "run", "start"]
基于 node:20-slim,镜像体积小,启动快。
项目设计文档
整体架构
┌─────────────────────────────────────────────────────────────┐
│ 全栈 Web 应用架构 │
├─────────────────────────────────────────────────────────────┤
│ 前端 (client/) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ React 18 + TypeScript + React Router v7 │ │
│ │ Vite 7 构建 + Less 样式 + HMR 热更新 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ HTTP API │
│ 后端 (server/) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Express 4 + TypeScript + tsx 运行时 │ │
│ │ Winston 日志 + 中间件链式处理 │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 开发工具: ESLint + Husky + lint-staged │
│ 部署方式: Docker (node:20-slim) │
└─────────────────────────────────────────────────────────────┘
目录职责
| 目录 | 职责 |
|---|---|
client/pages/ | 页面组件,一个文件对应一个路由 |
client/components/ | 可复用 UI 组件 |
client/hooks/ | 自定义 React Hooks |
client/utils/ | 前端工具函数(请求封装、XSS 过滤等) |
server/middleware/ | Express 中间件(路由、日志、错误处理等) |
server/utils/ | 后端工具函数(日志、格式化等) |
开发流程
-
启动开发环境
npm run dev -
添加新页面
- 在
client/pages/创建页面组件 - 在
client/App.tsx添加路由
- 在
-
添加新接口
- 在
server/middleware/serveApi.ts添加路由处理 - 前端使用
client/utils/request.ts调用
- 在
-
构建部署
npm run build:client # 构建前端 docker build -t my-app . # 构建镜像
总结:AI 时代的全栈复兴
回到最初的问题:为什么要从 SPA 升级到全栈架构?
答案是:AI。
当 AI 成为开发的重要辅助工具时,代码的可理解性变得前所未有的重要。AI 需要看到完整的上下文才能给出高质量的建议:
- 前端表单结构 → 后端参数校验
- 数据库表结构 → API 返回格式
- 业务逻辑 → 错误处理
前后端分离的架构,人为地把这些关联信息切割到了不同的仓库,AI 只能"盲人摸象"。
而全栈架构,把所有相关代码放在一个仓库里,AI 可以:
- 根据后端接口自动生成前端调用代码
- 根据数据库模型自动生成表单验证
- 根据业务逻辑自动补全错误处理
这不是技术倒退,而是在新工具面前的架构演进。
所谓分久必合,合久必分
当然,全栈架构不是银弹。对于大型团队、复杂业务,微服务架构仍然有其价值。但对于中小型项目、快速迭代的业务,全栈架构 + AI 辅助开发,绝对是效率最优解。
笔者在此澄清一点,原有的 Java 后端服务仍然在线上提供支持,只是新增的功能涉及到后端开发时会改为 nodejs 实现 。
最后预测一下:在 AI 时代,全栈开发者会越来越吃香。不是说要精通前后端所有技术,而是要有全局视角,能够在 AI 的辅助下,快速完成端到端的功能开发。
前端同学们,是时候往全栈方向卷一卷了~
本文项目源码已开源,欢迎 Star:fullstack-web-app