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' })

预防措施:

  • 开发前仔细阅读 Taro 官方文档
  • 参考项目 CLAUDE.md 中的 API 使用规范
  • 代码审查时检查是否使用了 Web API

2. SessionID 认证机制的理解偏差

教训: SessionID 前端不用于判断登录状态,而是传递给后端的凭证。

问题:

  • 前端通过 sessionid 判断用户是否登录(错误)
  • sessionid 过期后前端无法及时感知

正确理解:

// ❌ 错误 - 前端判断登录状态
const isLoggedIn = !!getSessionId()

// ✅ 正确 - 后端通过 401 判断
// 前端只需在请求中携带 sessionid
// 后端返回 401 时前端清除 sessionid 并跳转登录

预防措施:

  • 阅读 API 集成日志
  • 参考 src/utils/request.js 中的认证处理
  • 401 响应由后端判断,前端统一处理

3. API 响应格式检查错误

教训: 必须检查 res.code === 1,而不是 res.coderes.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