从零搭建一个现代化前端项目,我踩过的坑和学到的招
上周五晚上十点半,我还在公司调试一个 React 组件的 hydration 错误,窗外中关村的写字楼一盏盏熄灯,而我的 VSCode 里插件提示又弹出三条 lint 警告。作为一名在医疗行业软件公司干了三年的 Python 开发,你可能会问:“你不是写后端的吗?怎么搞起前端来了?”
说来话长。我们公司做的是医院信息系统(HIS)和临床决策支持系统,后端用 FastAPI + PostgreSQL,但前端一直是个“历史遗留问题”——老系统是 jQuery + Bootstrap 3 写的,页面跳转像坐过山车,动不动就白屏。去年底,产品总监拍板要重构整个医生工作站界面,目标是“像 DeepSeek 那样的智能交互体验”。没错,就是那个最近火出圈的 AI 公司 DeepSeek,他们家的 Web 界面流畅、响应快、还有自然语言交互,产品经理拿它当标杆,天天在会上念叨。
于是,作为团队里“唯一会点前端”的后端工程师(其实只是能看懂 React 语法),我被推上了前线。领导说:“你不是喜欢研究底层原理吗?这回正好练手。”行吧,谁让我住在回龙观,通勤一小时,加班到深夜也习惯了呢。
为什么不能直接用 create-react-app?
一开始我想偷懒,直接 npx create-react-app my-clinic-ui 完事。但很快发现不行:
- 我们需要 TypeScript 强类型约束,医疗数据容不得半点马虎;
- 必须支持微前端架构,因为老系统要并行运行半年;
- 需要极致的性能优化,医生可没耐心等三秒加载;
- 还得集成 Web Workers 做本地 AI 推理(对接 DeepSeek 的轻量模型)。
CRA 太“黑盒”了,webpack 配置锁死,想改个别名都得 eject,一旦 eject 就再也回不去了。这就像给 ICU 患者打止痛药——短期舒服,长期致命。
所以,我决定从零手搓一个现代化前端项目脚手架。不是为了炫技,而是为了可控。
技术选型:务实大于潮流
我列了个需求清单,拉着前端实习生(刚毕业的小王)一起讨论:
| 需求 | 选型 | 理由 |
|---|---|---|
| 框架 | React 18 + Vite | 快!开发服务器启动 < 500ms,HMR 秒级更新 |
| 状态管理 | Zustand | 医疗场景状态复杂但不需要 Redux 那套样板代码 |
| UI 库 | Ant Design Pro | 已有医疗主题色,且支持国际化(我们有海外客户) |
| 路由 | React Router v6 | 嵌套路由清晰,适合多级菜单的医生工作站 |
| 构建工具 | Vite + esbuild | 比 Webpack 快 10-100 倍,尤其适合大项目 |
| 代码规范 | ESLint + Prettier + Husky | 提交前自动格式化,避免“缩进战争” |
| 测试 | Vitest + React Testing Library | Vite 原生支持,跑测试快如闪电 |
特别说明:没选 Next.js。虽然它很火,但我们是纯 SPA,不需要 SSR,而且微前端集成会更复杂。Vite 的开发体验真的香,第一次用的时候我以为电脑卡了——怎么保存文件界面就变了?后来才知道是 HMR 太快了。
项目结构:清晰比炫酷重要
我见过太多前端项目把 components/ 里塞了几百个文件,找一个按钮组件要翻半天。这次我坚持按 功能域(Feature-based) 划分:
src/
├── features/ # 核心业务功能
│ ├── patient-search/ # 患者搜索
│ ├── order-entry/ # 医嘱录入
│ └── ai-assist/ # DeepSeek 集成入口
├── shared/ # 跨功能复用
│ ├── ui/ # 通用组件(Button, Card...)
│ ├── lib/ # 工具函数
│ └── hooks/ # 自定义 Hook
├── app/ # 应用骨架
│ ├── providers/ # Context Provider
│ ├── routes/ # 路由配置
│ └── layout/ # 主布局
├── assets/ # 静态资源
└── main.tsx # 入口
这样,新同事接手时,只要知道“医嘱录入”在哪个目录,就能快速定位。小王一开始还想按“components / pages / utils”分,被我用“你愿意在 200 个组件里找一个叫 PatientCard 的文件吗?”说服了。
DeepSeek 集成:不只是加个 API 调用
DeepSeek 的前端 SDK 很简单,但医疗场景有特殊要求:
- 数据脱敏:患者姓名、病历号不能直接发给第三方;
- 离线可用:医院网络不稳定,AI 功能需降级为本地规则引擎;
- 审计日志:所有 AI 交互必须记录,满足 HIPAA 合规。
于是我在 ai-assist 模块里做了三层封装:
// features/ai-assist/lib/aiClient.ts
export const createAIClient = (config: AIConfig) => {
// 1. 请求前脱敏
const sanitizeInput = (text: string) => {
return text.replace(/[\u4e00-\u9fa5]{2,3}(?=[\u4e00-\u9fa5])/g, '**'); // 中文姓名脱敏
};
// 2. 尝试调用 DeepSeek
const callDeepSeek = async (prompt: string) => {
try {
const cleanPrompt = sanitizeInput(prompt);
const res = await fetch('https://api.deepseek.com/chat', {
method: 'POST',
body: JSON.stringify({ message: cleanPrompt }),
headers: { 'Authorization': `Bearer ${config.apiKey}` }
});
if (!res.ok) throw new Error('DeepSeek API error');
return await res.json();
} catch (err) {
// 3. 降级到本地规则
console.warn('Falling back to local rules', err);
return fallbackToLocalRules(prompt);
}
};
return { callDeepSeek };
};
同时,用 Web Worker 避免阻塞主线程:
// features/ai-assist/worker/aiWorker.ts
self.onmessage = async (e) => {
const { prompt, config } = e.data;
const client = createAIClient(config);
const result = await client.callDeepSeek(prompt);
self.postMessage(result);
};
在 React 组件中使用:
// features/ai-assist/ui/AIAssistPanel.tsx
import { useEffect, useRef, useState } from 'react';
export const AIAssistPanel = () => {
const workerRef = useRef<Worker | null>(null);
const [response, setResponse] = useState('');
useEffect(() => {
workerRef.current = new Worker(new URL('./aiWorker.ts', import.meta.url));
workerRef.current.onmessage = (e) => setResponse(e.data);
return () => workerRef.current?.terminate();
}, []);
const handleAsk = (prompt: string) => {
workerRef.current?.postMessage({
prompt,
config: { apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY }
});
};
return (
<div>
<input onKeyPress={(e) => e.key === 'Enter' && handleAsk(e.currentTarget.value)} />
<div>{response || '思考中...'}</div>
</div>
);
};
上线前,测试同学差点把我骂死:“你这 AI 回答‘建议做阑尾切除’,结果患者是妇科病!”——原来脱敏把“张**”和“李**”混了,导致上下文错乱。赶紧加了实体识别模块,这才过关。
性能优化:医生等不起
医疗系统最怕卡顿。我做了几件事:
- 代码分割:用
React.lazy+Suspense按路由拆包
const PatientSearch = lazy(() => import('@/features/patient-search'));
- 图片懒加载:用
loading="lazy"原生属性,比第三方库轻量 - 防抖搜索:患者搜索框输入延迟 300ms 再请求
- 虚拟滚动:医嘱列表上千条,用
react-window只渲染可视区域
最狠的是预加载关键路由。在用户登录后,我就悄悄加载医生工作站首页的 chunk:
// app/routes/preload.ts
export const preloadCriticalRoutes = () => {
if (process.env.NODE_ENV === 'production') {
import('@/features/order-entry').then(() => console.log('Preloaded order entry'));
import('@/features/ai-assist').then(() => console.log('Preloaded AI assist'));
}
};
实测首屏加载从 2.8s 降到 0.9s,产品经理终于闭嘴了。
开发体验:VSCode 插件救我狗命
作为一个重度 VSCode 用户,我装了这些插件提升效率:
- ESLint:实时标红,提交前自动 fix
- Prettier:保存即格式化,团队风格统一
- Path Intellisense:自动补全
@/shared/ui/Button路径 - Error Lens:把错误信息直接显示在代码行上
- REST Client:不用 Postman,直接在
.http文件里调 DeepSeek API
还有个私藏技巧:用 console.log 调试时,加上 // @ts-ignore 避免 ESLint 报错,但提交前用 husky 的 pre-commit hook 自动删除所有 console.log。再也不用担心线上留调试代码了。
上线后的反思
项目上线三个月,医生反馈“比老系统快多了,AI 也挺聪明”。但我也踩了不少坑:
- 别过度设计:一开始想用 Redux Toolkit,结果 Zustand 二十行代码搞定;
- 浏览器兼容性:某三甲医院还在用 IE11(别笑,真的!),最后加了 core-js polyfill;
- TypeScript 类型地狱:医疗数据结构嵌套太深,写类型定义写了两天,差点放弃。
不过最大的收获是:前端不再是“切图仔”,而是产品体验的核心。DeepSeek 的成功,一半靠算法,一半靠前端把 AI 能力无缝融入工作流。我们虽然做不到那么智能,但至少让医生少点几次鼠标。
给同行的建议
如果你也在传统行业(医疗、金融、制造)做前端,我的建议是:
- 先解决痛点,再追求技术:医生不在乎你用 Vite 还是 Webpack,只在乎能不能三秒内开出医嘱;
- 合规性优先:医疗数据隐私是红线,任何第三方服务都要评估;
- 和后端对齐:我们和 Python 后端约定 API 用 OpenAPI 3.0 描述,前端用 openapi-typescript 自动生成 TS 类型,省了无数沟通成本。
最后,别怕从零开始。手写配置虽然累,但你能真正理解每一行代码的作用。就像我每天通勤一小时,看似浪费时间,其实路上听技术播客、想架构设计,反而成了充电时刻。
哦对了,现在我的 VSCode 插件列表已经 37 个了,产品经理说下次要加语音输入功能……看来又得研究 Web Speech API 了。程序员的命,就是不断造轮子啊。
(完)

评论 0