lessons-learned.md
7.98 KB
经验教训总结
更新时间: 2026-02-05
本文档记录项目开发过程中的经验教训,避免重复踩坑。
🔥 核心教训
1. 小程序 ≠ Web + 适配器
教训: Taro 不是简单的"Web + 适配器",而是完全不同的开发范式。
问题:
- 直接使用
localStorage导致小程序崩溃 - 使用
window.document无法操作 DOM - 使用
fetch发起请求失败
解决方案:
// ❌ 错误 - 使用 Web API
localStorage.setItem('key', 'value')
window.document.getElementById()
fetch('/api/data')
// ✅ 正确 - 使用 Taro API
Taro.setStorage({ key: 'key', data: 'value' })
Taro.createSelectorQuery()
Taro.request({ url: '/api/data' })
预防措施:
2. SessionID 认证机制的理解偏差
教训: SessionID 前端不用于判断登录状态,而是传递给后端的凭证。
问题:
- 前端通过 sessionid 判断用户是否登录(错误)
- sessionid 过期后前端无法及时感知
正确理解:
// ❌ 错误 - 前端判断登录状态
const isLoggedIn = !!getSessionId()
// ✅ 正确 - 后端通过 401 判断
// 前端只需在请求中携带 sessionid
// 后端返回 401 时前端清除 sessionid 并跳转登录
预防措施:
- 阅读 API 集成日志
- 参考
src/utils/request.js中的认证处理 - 401 响应由后端判断,前端统一处理
3. API 响应格式检查错误
教训: 必须检查 res.code === 1,而不是 res.code 或 res.data。
问题:
// ❌ 错误 - 不检查或错误检查
const res = await userAPI()
if (res.code) { // 错误:0 也是 truthy
// 处理成功
}
解决方案:
// ✅ 正确 - 严格检查 === 1
const res = await userAPI()
if (res.code === 1) {
// 处理成功
} else {
// 处理业务错误
Taro.showToast({ title: res.msg, icon: 'none' })
}
预防措施:
- 所有 API 调用都检查
res.code === 1 - 代码审查时重点检查此模式
- 参考 代码规范
4. 双设计宽度配置的必要性
教训: NutUI 组件和普通内容需要不同的设计宽度。
问题:
- NutUI 组件使用 750px 设计稿时尺寸过大
- 普通内容使用 375px 设计稿时尺寸过小
解决方案:
// config/index.js
designWidth (input) {
// NutUI 组件:375px
if (input?.file?.indexOf('@nutui') > -1) {
return 375
}
// 其他内容:750px(Taro 标准)
return 750
}
预防措施:
- 项目初始化时配置好双设计宽度
- 新增 NutUI 组件时注意尺寸问题
- 参考项目配置文件
5. 生命周期 Hook 的正确使用
教训: Taro 页面组件必须使用 Taro 生命周期 Hook,而非 Vue 生命周期。
问题:
// ❌ 错误 - 使用 Vue 生命周期
import { onMounted } from 'vue'
onMounted(() => {
// 可能不按预期工作
})
解决方案:
// ✅ 正确 - 使用 Taro 生命周期
import { useLoad, useShow, useReady } from '@tarojs/taro'
useLoad((options) => {
// 页面加载(只触发一次)- 适合获取路由参数
})
useShow(() => {
// 页面显示(每次显示都触发)- 适合刷新数据
})
useReady(() => {
// 页面首次渲染完成
})
预防措施:
- 页面组件始终使用
useLoad/useShow/useReady - Vue 生命周期仅用于非页面组件
- 参考项目 CLAUDE.md
📚 最佳实践总结
1. 组件拆分原则
原则: 单一职责、可复用、易测试。
示例:
✅ 好的组件
- UserCard.vue - 展示用户信息
- PointsCollector.vue - 积分收集器
- FamilyAlbum.vue - 家庭相册
❌ 避免的组件
- UserProfileAndSettingsAndOrders.vue - 职责混乱
- BigComponent.vue - 难以维护和测试
2. API 调用封装
原则: 统一封装、错误处理、加载状态。
示例:
// ✅ 好的封装
const fetchData = async () => {
loading.value = true
try {
const res = await yourAPI()
if (res.code === 1) {
dataList.value = res.data
} else {
Taro.showToast({ title: res.msg, icon: 'none' })
}
} catch (err) {
Taro.showToast({ title: '网络异常', icon: 'none' })
} finally {
loading.value = false
}
}
3. 样式管理
原则: TailwindCSS 优先(80%)、Less 补充(20%)。
示例:
<template>
<!-- TailwindCSS 用于布局、间距、颜色 -->
<view class="flex items-center justify-between p-4 bg-white">
<text class="text-xl font-bold">标题</text>
</view>
</template>
<style lang="less" scoped>
/* Less 用于深度选择器、动画 */
.custom-element :deep(.nut-popup) {
background-color: #fff;
}
@keyframes slide-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
</style>
4. 命名规范
原则: 清晰、一致、符合惯例。
示例:
// 组件:PascalCase
UserCard.vue
PointsCollector.vue
// 函数:camelCase + 动词开头
const fetchData = () => {}
const handleSubmit = () => {}
const formatDate = () => {}
// 变量:camelCase
const userList = ref([])
const isLoading = ref(false)
// 常量:UPPER_CASE
const MAX_COUNT = 100
const API_BASE_URL = 'https://api.example.com'
🚫 常见陷阱
陷阱 1: 直接修改 props
// ❌ 错误
props.userName = 'new name'
// ✅ 正确
emit('update:userName', 'new name')
陷阱 2: 解构 props 丢失响应性
// ❌ 错误
const { userName } = props
// ✅ 正确
const { userName } = toRefs(props)
陷阱 3: 在模板中调用方法
<!-- ❌ 错误 - 每次渲染都执行 -->
<div>{{ formatDate(item.date) }}</div>
<!-- ✅ 正确 - 使用 computed -->
<div>{{ formattedDate }}</div>
陷阱 4: 滥用 deep: true
// ❌ 性能问题
watch(largeObject, handler, { deep: true })
// ✅ 优化 - 监听具体属性
watch(() => largeObject.value.nested.prop, handler)
💡 性能优化建议
1. 长列表优化
// 使用虚拟滚动(如有大量数据)
// 或使用分页加载
const page = ref(1)
const loadMore = async () => {
const res = await getMoreData(page.value)
dataList.value.push(...res.data)
page.value++
}
2. 图片优化
// CDN 图片优化
function optimizeImageUrl(url, width = 750, quality = 70) {
if (!url.includes('cdn.ipadbiz.cn')) return url
return `${url}?imageMogr2/thumbnail/${width}x/quality/${quality}`
}
3. 计算属性缓存
// ✅ 使用 computed(缓存)
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0)
})
// ❌ 避免方法(每次重新计算)
const getTotalPrice = () => {
return items.value.reduce((sum, item) => sum + item.price, 0)
}
📖 推荐阅读
项目文档
外部资源
🔄 持续更新
本文档会持续更新,记录新的经验教训。
更新频率: 每次遇到重要问题后更新 维护者: 开发团队 最后更新: 2026-02-05
📝 如何添加新经验
遇到新的问题后,按以下格式添加到本文档:
### 问题标题
**教训**: 简短描述
**问题**:
- 问题描述
**解决方案**:
```javascript
// 代码示例
预防措施:
- 如何避免 ```
维护者: 开发团队 最后更新: 2026-02-05