
见过太多这样的场景了:
产品经理:"这个需求很简单,改一下就行。" 你打开代码:"卧槽,这是谁写的?" Git Blame 一查:"好像是我自己……半年前写的。"
残酷的真相是:90%的前端项目从第一行代码开始,就埋下了半年后重构的种子。
不是你技术不行,而是大多数人根本不知道什么叫"面向未来编程"。今天我们就来拆解前端代码走向死亡的9大致命陷阱,以及如何用架构思维让代码活得更久。
很多人以为"组件化 = 可复用",于是疯狂拆组件:
// 看起来很"工程化",实则埋下隐患
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchUser().then(setUser); // API调用
}, []);
const handleEdit = () => {
// 业务逻辑
updateUser(user);
};
return (
<div>
{/* UI逻辑 */}
<Avatar src={user?.avatar} />
<Form data={user} onSubmit={handleEdit} />
</div>
);
}
问题在哪? 这个组件混合了:
半年后需求变化:
// 1. 服务层 - 隔离外部依赖
// services/userService.ts
exportconst userService = {
fetchUser: () => apiClient.get('/user'),
updateUser: (data) => apiClient.put('/user', data)
};
// 2. 行为层 - 封装业务逻辑
// hooks/useUserProfile.ts
exportfunction useUserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const loadUser = useCallback(async () => {
setLoading(true);
try {
const data = await userService.fetchUser();
setUser(data);
} finally {
setLoading(false);
}
}, []);
const updateProfile = useCallback(async (updates) => {
const updated = await userService.updateUser(updates);
setUser(updated);
}, []);
return { user, loading, loadUser, updateProfile };
}
// 3. UI层 - 纯展示组件
function UserProfile() {
const { user, loading, loadUser, updateProfile } = useUserProfile();
return<UserProfileView
user={user}
loading={loading}
onRefresh={loadUser}
onUpdate={updateProfile}
/>;
}
分层的威力:
useUserProfileuserService 即可userService朋友在一家做教育SaaS的公司,他们有个表单组件:
// 最开始:只支持邮箱验证
function EmailInput() {
const [email, setEmail] = useState('');
const validate = (value) => {
return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
};
const handleChange = (e) => {
const value = e.target.value;
if (validate(value)) {
setEmail(value);
props.onChange?.(value);
}
};
return<input type="email" value={email} onChange={handleChange} />;
}
一个月后,产品说:"我们要支持手机号登录。"
于是他又写了个 PhoneInput 组件,复制粘贴改正则。
两个月后,产品说:"表单输入要支持实时提示,比如'密码强度太弱'。"
他又改了一遍 EmailInput 和 PhoneInput。
三个月后,产品说:"输入框要支持前后缀图标。" 这时候他发现,项目里已经有8个几乎一样的输入框组件了,每次改需求要同时改8个地方。
问题根源: 没有为扩展留余地,所有逻辑都写死了。
// 核心思想:通过配置注入行为,而非修改代码
function ValidatedInput({
type = 'text',
validators = [],
transformers = [],
...props
}) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
let newValue = e.target.value;
// 应用转换器(可扩展)
transformers.forEach(transform => {
newValue = transform(newValue);
});
// 应用验证器(可扩展)
for (const validator of validators) {
const result = validator(newValue);
if (!result.valid) {
setError(result.message);
return;
}
}
setError('');
setValue(newValue);
};
return (
<>
<input value={value} onChange={handleChange} {...props} />
{error && <span className="error">{error}</span>}
</>
);
}
// 使用示例 - 无需修改组件代码
<ValidatedInput
validators={[
emailValidator,
maxLengthValidator(50)
]}
transformers={[
trimWhitespace,
toLowerCase
]}
/>
关键原则:开闭原则(OCP)
对扩展开放,对修改关闭
新来的实习生问我:"哥,我想改一下订单列表的筛选逻辑,应该改哪个文件?"
我:"emmm,你先去 components 文件夹找 OrderList.tsx……"
他找了半天:"找到了,但是这里面没有筛选逻辑啊?"
我:"哦对,筛选逻辑在 hooks 文件夹的 useOrderFilter.ts 里。"
他:"那接口调用呢?"
我:"在 services 文件夹的 orderService.ts……对了,还有类型定义在 types 文件夹的 order.d.ts。"
他:"……"(已经懵了)
典型的按类型分类:
src/
components/
Button.tsx
Modal.tsx
UserForm.tsx
OrderList.tsx
ProductCard.tsx
... (100个组件混在一起)
hooks/
useUser.ts
useOrder.ts
useProduct.ts
... (50个hooks找不到)
services/
userService.ts
orderService.ts
...
问题在哪?
src/
features/
user/
components/
UserForm.tsx
UserAvatar.tsx
hooks/
useUserProfile.ts
useUserAuth.ts
services/
userService.ts
types/
user.types.ts
index.ts // 统一导出
order/
components/
hooks/
services/
index.ts
shared/
ui/ // 通用UI组件
hooks/ // 通用Hooks
utils/ // 工具函数
优势:
去年有个朋友跳槽到一家做B端SaaS的公司,他们的项目是3年前用React写的。现在公司想:
结果呢?一行代码都复用不了。
为什么?因为业务逻辑和React深度耦合:
// 典型的"React全家桶"式写法
function OrderManager() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(false);
// 业务逻辑全写在useEffect里
useEffect(() => {
setLoading(true);
fetch('/api/orders')
.then(res => res.json())
.then(data => {
// 筛选逻辑
const active = data.filter(o => o.status === 'active');
// 排序逻辑
const sorted = active.sort((a, b) => b.createTime - a.createTime);
setOrders(sorted);
})
.finally(() => setLoading(false));
}, []);
const handleApprove = useCallback((id) => {
// 审批逻辑
fetch(`/api/orders/${id}/approve`, { method: 'POST' })
.then(() => {
setOrders(prev => prev.map(o =>
o.id === id ? { ...o, status: 'approved' } : o
));
});
}, []);
return<OrderList data={orders} onApprove={handleApprove} />;
}
这段代码的问题:
// ✅ 第一步:业务逻辑用纯JS/TS写(和框架无关)
// domain/OrderManager.ts
exportclass OrderManager {
constructor(private apiClient) {}
// 纯业务逻辑,不依赖任何框架
async getActiveOrders() {
const orders = awaitthis.apiClient.get('/orders');
return orders
.filter(o => o.status === 'active')
.sort((a, b) => b.createTime - a.createTime);
}
async approveOrder(orderId) {
awaitthis.apiClient.post(`/orders/${orderId}/approve`);
return { success: true };
}
calculateTotal(orders) {
return orders.reduce((sum, o) => sum + o.amount, 0);
}
}
// ✅ 第二步:React适配层(只负责连接UI和业务逻辑)
// adapters/react/useOrderManager.ts
exportfunction useOrderManager() {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(false);
// 创建业务逻辑实例
const manager = useMemo(() =>
new OrderManager(apiClient),
[]
);
const loadOrders = useCallback(async () => {
setLoading(true);
try {
const data = await manager.getActiveOrders();
setOrders(data);
} finally {
setLoading(false);
}
}, [manager]);
return { orders, loading, loadOrders, manager };
}
// ✅ 使用
function OrderPage() {
const { orders, loading, loadOrders } = useOrderManager();
return<OrderList data={orders} loading={loading} />;
}
这样做的好处:
// 换成Vue也很简单
// adapters/vue/useOrderManager.js
import { ref, onMounted } from'vue';
exportfunction useOrderManager() {
const orders = ref([]);
const loading = ref(false);
const manager = new OrderManager(apiClient);
const loadOrders = async () => {
loading.value = true;
try {
orders.value = await manager.getActiveOrders();
} finally {
loading.value = false;
}
};
return { orders, loading, loadOrders };
}
我司的实践: 我们用这个思路改造了核心业务模块,后来做小程序时,业务逻辑直接复用了80%,开发时间省了一个月。
上周我朋友公司出了个事故:
事后复盘:那段代码没有任何测试覆盖,改动影响范围完全靠"猜"。
没有测试的代码 = 不敢改的代码 = 遗留代码
很多团队觉得"前端不用写测试",直到:
我调研了几个团队:
第一步:给核心业务逻辑加单元测试
// 测试业务逻辑(不依赖UI)
import { OrderManager } from'@/domain/OrderManager';
describe('订单管理', () => {
let manager;
let mockApi;
beforeEach(() => {
// Mock API,不用真的调后端
mockApi = {
get: jest.fn(),
post: jest.fn()
};
manager = new OrderManager(mockApi);
});
test('应该正确筛选和排序活跃订单', async () => {
// 准备测试数据
mockApi.get.mockResolvedValue([
{ id: 1, status: 'active', createTime: 100 },
{ id: 2, status: 'closed', createTime: 200 },
{ id: 3, status: 'active', createTime: 300 }
]);
const orders = await manager.getActiveOrders();
// 验证结果
expect(orders).toHaveLength(2);
expect(orders[0].id).toBe(3); // 应该按时间倒序
expect(orders[1].id).toBe(1);
});
test('计算订单总额应该正确', () => {
const orders = [
{ amount: 100 },
{ amount: 200 },
{ amount: 50 }
];
const total = manager.calculateTotal(orders);
expect(total).toBe(350);
});
});
第二步:给用户操作加集成测试
import { render, fireEvent, waitFor, screen } from'@testing-library/react';
import { OrderPage } from'@/features/order/OrderPage';
test('用户应该能够审批订单', async () => {
// 渲染页面
render(<OrderPage />);
// 等待订单加载
await waitFor(() => {
expect(screen.getByText('待审批订单')).toBeInTheDocument();
});
// 点击"审批"按钮
const approveButton = screen.getByRole('button', { name: /审批/ });
fireEvent.click(approveButton);
// 确认弹窗出现
expect(screen.getByText('确认审批?')).toBeInTheDocument();
// 点击确认
fireEvent.click(screen.getByRole('button', { name: /确认/ }));
// 验证成功提示
await waitFor(() => {
expect(screen.getByText('审批成功')).toBeInTheDocument();
});
});
第三步:循序渐进,不要一次搞太多
我给团队定的规则:
效果: 三个月后,测试覆盖率从0%到了45%,线上bug率下降了60%。
接手一个项目,打开代码:
// 文件A:老王写的
class UserList extends React.Component {
componentDidMount() {
fetch('/api/users').then(res => {
this.setState({ users: res.data })
})
}
}
// 文件B:小李写的
const OrderList = () => {
const [orders, setOrders] = useState([])
useEffect(() => {
axios.get('/api/orders').then(({data}) => setOrders(data));
}, [])
}
// 文件C:实习生写的
function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => {
request({
url: '/api/products',
method: 'get'
}).then(res => {
setProducts(res.list)
})
}, [])
}
同一个项目里:
结果: 代码库像"拼接怪",新人看代码都要先猜"这是谁的风格"。
我见过最夸张的:一个10人团队的项目,同一个功能的实现方式有5种不同写法。
为什么?因为:
第一步:配置ESLint + Prettier
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended'
],
rules: {
// 禁止直接使用fetch
'no-restricted-globals': ['error', {
name: 'fetch',
message: '请使用 @/utils/request 代替 fetch'
}],
// 禁止使用var
'no-var': 'error',
// 必须使用命名导出(不用export default)
'import/no-default-export': 'error',
// useState必须有类型注解(如果用TS)
'@typescript-eslint/explicit-function-return-type': 'warn'
}
};
// prettier.config.js
module.exports = {
printWidth: 100,
semi: true, // 统一使用分号
singleQuote: true, // 统一使用单引号
trailingComma: 'es5',
tabWidth: 2
};
第二步:Git提交前自动检查(Husky)
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
效果:
第三步:写一份团队规范文档
# 开发规范
## API调用
✅ 使用统一的 request 工具
❌ 不要直接用 fetch 或 axios
## 组件编写
✅ 优先使用函数组件 + Hooks
❌ 不要新写 Class 组件
## 状态管理
- 本地状态:useState
- 跨组件状态:Zustand
- 服务端数据:React Query
## 命名规范
- 组件文件:PascalCase(UserProfile.tsx)
- 工具函数:camelCase(formatDate.ts)
- 常量:UPPER_SNAKE_CASE(API_BASE_URL)
我们团队的实践: 规范落地后,Code Review时间从平均1小时降到了20分钟,因为大家不用再讨论风格问题,可以专注于逻辑本身。
新人小张入职第一天:
小张:"这个项目的状态管理是怎么设计的?" 老李:"emmm,你看代码就知道了。"
小张(看了一天代码):"为什么有的地方用Redux,有的地方用Context,有的又用Zustand?" 老李:"哦,Redux是历史遗留的,Context是临时方案,Zustand是我们现在推荐的。" 小张:"那我新功能用哪个?" 老李:"看情况吧……"
小张内心:"我看个锤子啊……"
我见过很多团队:
结果:
很多人一提文档就头疼:"要写多少文档啊?我代码都写不完……"
我的建议:不要写厚厚的文档,只写3个关键的。
1. 架构决策文档(ADR - Architecture Decision Record)
# docs/architecture/001-状态管理方案.md
## 背景
项目初期用了Redux,但团队反馈太繁琐。
## 决策
- 本地状态:useState / useReducer
- 全局状态:Zustand(轻量、简单)
- 服务端数据:React Query(自动缓存、重试)
## 理由
1. Zustand比Redux简单,学习成本低
2. React Query专门处理异步数据,不用手写loading状态
3. 本地状态够用就不要全局
## 示例
\`\`\`typescript
// ✅ 本地状态
const [count, setCount] = useState(0);
// ✅ 全局状态(跨页面共享)
import { useAuthStore } from '@/stores/auth';
const { user, login, logout } = useAuthStore();
// ✅ 服务端数据
import { useQuery } from '@tanstack/react-query';
const { data: orders } = useQuery(['orders'], fetchOrders);
\`\`\`
## 更新日期
2024-03-15
2. 快速上手指南
# docs/快速开始.md
## 开发一个新功能的标准流程
### 1. 确定功能位置
新功能放在 `src/features/` 下:
\`\`\`
src/features/新功能名/
components/ # UI组件
hooks/ # 业务逻辑
services/ # API调用
index.ts # 统一导出
\`\`\`
### 2. 创建组件
\`\`\`typescript
// components/FeatureName.tsx
export function FeatureName() {
const { data, loading } = useFeatureData();
return <FeatureView data={data} loading={loading} />;
}
\`\`\`
### 3. 添加路由
在 `src/routes/index.ts` 添加路由配置
### 4. 提交代码
\`\`\`bash
git add .
git commit -m "feat: 添加XX功能" # 会自动检查代码规范
git push
\`\`\`
3. 常见问题FAQ
# docs/FAQ.md
## Q: 为什么不能直接用 fetch?
A: 我们封装了统一的 request 工具(`@/utils/request`),它自动处理:
- Token添加
- 错误拦截
- 请求日志
- 超时控制
## Q: 新增接口怎么做类型定义?
A: 在对应功能的 `types/` 目录下添加,参考 `features/user/types/user.types.ts`
## Q: 本地开发如何调试接口?
A: `.env.development` 中配置了代理,会自动转发到测试环境
## Q: 遇到奇怪的bug怎么办?
A: 先看浏览器控制台,再看 Sentry 错误日志
效果: 我们团队只维护这3个文档,新人上手时间从2周缩短到3天。关键是文档短小精悍,不会因为太长而没人看。
真事:我一个朋友公司的C端产品,某个核心功能崩溃了整整3天,直到用户在微博上吐槽才发现。
为什么?
后果:
我问过很多前端团队:"你们有监控吗?"
回答五花八门:
第一步:接入错误监控(推荐用国内服务)
国内可用的监控平台:
// 接入阿里云ARMS(示例)
import arms from'arms-front';
// 初始化
arms.init({
pid: 'your-project-id',
region: 'cn-hangzhou'
});
// 在React里加错误边界
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// 自动上报到ARMS
arms.error(error, {
component: errorInfo.componentStack,
userId: getCurrentUser()?.id,
page: window.location.href
});
}
render() {
if (this.state.hasError) {
return<div>页面出错了,请刷新重试</div>;
}
returnthis.props.children;
}
}
第二步:监控核心性能指标
// 监控页面加载性能
import { reportWebVitals } from'web-vitals';
reportWebVitals((metric) => {
// 上报到监控平台
arms.send({
type: 'performance',
name: metric.name, // LCP、FID、CLS等
value: metric.value,
page: window.location.pathname
});
// 如果性能太差,打个警告
if (metric.name === 'LCP' && metric.value > 2500) {
console.warn('⚠️ 页面加载太慢了!', metric);
}
});
第三步:关键操作埋点
// 工具函数:统一埋点
exportfunction trackEvent(eventName, properties = {}) {
// 上报到监控平台
arms.track(eventName, {
...properties,
timestamp: Date.now(),
userId: getCurrentUser()?.id,
page: window.location.pathname
});
}
// 使用示例
function OrderButton() {
const handleSubmit = () => {
// 关键操作要埋点
trackEvent('订单提交', {
orderId: order.id,
amount: order.amount
});
submitOrder();
};
return<button onClick={handleSubmit}>提交订单</button>;
}
第四步:设置告警规则
在监控平台配置:
成本:
收益:
建议: 如果预算有限,至少要有错误监控,其他可以慢慢加。
去年接手一个项目,产品说:"把所有按钮改成圆角的。"
我:"好的……等等,我们有多少个Button组件?"
Git搜索结果:
Button.tsx(最早的)CustomButton.tsx(不知道谁写的)PrimaryButton.tsx、SecondaryButton.tsx(按样式分的)UserButton.tsx、OrderButton.tsx(按功能分的)ButtonV2.tsx(不知道V1在哪)NewButton.tsx、TempButton.tsx(临时方案)总共37个"按钮"组件!
为什么会这样?
很多团队:
结果: 永远在"计划重构",永远没时间做。
很多人的误区: "设计系统=要做一套完整的组件库,太复杂了!"
实际上: 从3个基础组件开始就够了。
第一步:定义设计Token
// design-tokens/colors.ts
exportconst colors = {
// 主色
primary: '#1890ff',
primaryHover: '#40a9ff',
primaryActive: '#096dd9',
// 功能色
success: '#52c41a',
warning: '#faad14',
error: '#ff4d4f',
// 中性色
text: {
primary: '#000000d9',
secondary: '#00000073',
disabled: '#00000040'
},
// 背景色
background: {
default: '#ffffff',
gray: '#fafafa',
hover: '#f5f5f5'
}
};
// design-tokens/spacing.ts
exportconst spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px'
};
// design-tokens/radius.ts
exportconst radius = {
small: '2px',
medium: '4px',
large: '8px',
round: '50%'
};
第二步:做3个基础组件
// components/Button/Button.tsx
import { colors, spacing, radius } from '@/design-tokens';
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function Button({
variant = 'primary',
size = 'medium',
children,
...props
}: ButtonProps) {
return (
<button
className={`btn btn--${variant} btn--${size}`}
{...props}
>
{children}
</button>
);
}
// 样式统一写在一起
const styles = {
'.btn': {
borderRadius: radius.medium,
border: 'none',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s'
},
// 变体样式
'.btn--primary': {
background: colors.primary,
color: '#fff',
'&:hover': {
background: colors.primaryHover
}
},
// 尺寸样式
'.btn--small': {
padding: `${spacing.xs} ${spacing.sm}`,
fontSize: '12px'
},
'.btn--medium': {
padding: `${spacing.sm} ${spacing.md}`,
fontSize: '14px'
}
};
第三步:强制使用
// .eslintrc.js(加个规则)
rules: {
'no-restricted-imports': ['error', {
patterns: [
{
group: ['antd/es/button', 'antd/lib/button'],
message: '请使用 @/components/Button'
}
]
}]
}
第四步:在Figma/蓝湖同步设计规范
和设计师约定:
我们团队的实践:
关键: 不要追求完美,先把最常用的3-5个组件统一就有巨大收益。
看到这里,可能有人会说:"这也太复杂了吧?我们小团队哪有精力搞这些?"
我想说:面向未来 ≠ 过度设计
需要分层吗? 看复杂度:
需要抽象吗? 看复用:
需要测试吗? 看风险:
需要文档吗? 看团队:
我的建议:
第一个月: 把基础打好
第二个月: 加点规范
第三个月: 提升质量
不要一开始就想着"重构整个项目",那样会被淹没。
根据我和50+团队交流的经验:
早期创业公司(<10人):
成长期公司(10-50人):
成熟公司(50人+):
一个不那么励志的真相: 大多数前端代码的寿命不超过2年。
不是因为技术过时,而是因为:
但是! 那些做得好的项目,可以用5年、10年,甚至更久。
它们的共同特点:
这才是"可持续发展"的代码。
写代码不只是给机器看的,更是给6个月后的自己看的。 如果6个月后的你看到今天的代码会说"牛逼",那就对了。 如果看到会说"这TM是谁写的",那就该改改思路了。
你的项目有遇到过这些坑吗? 评论区聊聊:
觉得有帮助的话,点个赞+收藏!
关注我,一起进步! 🚀