# Taro 小程序开发规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-10-15 | 初始Taro小程序开发规范 | Winston |

## 概述

本文档定义了出行服务项目中Taro小程序开发的规范和最佳实践，确保代码质量、一致性和可维护性。

## 技术栈配置

### 核心依赖版本
```json
{
  "@tarojs/taro": "4.1.4",
  "@tarojs/react": "4.1.4",
  "react": "^18.0.0",
  "tailwindcss": "^4.1.11",
  "weapp-tailwindcss": "^4.2.5"
}
```

### 多平台编译配置
```typescript
// config/index.ts
const platform = process.env.TARO_ENV || 'weapp'
const env = process.env.NODE_ENV || 'development'
const outputDir = `dist/${platform}/${env}`
```

## 项目结构规范

### 目录结构
```
mini/
├── src/
│   ├── pages/              # 页面组件
│   │   ├── index/         # 首页
│   │   ├── login/         # 登录页
│   │   └── profile/       # 个人中心
│   ├── components/        # 共享组件
│   │   └── ui/           # shadcn/ui组件库
│   ├── layouts/          # 布局组件
│   ├── utils/            # 工具函数
│   │   ├── auth.tsx      # 认证工具
│   │   ├── cn.ts         # className工具
│   │   └── platform.ts   # 平台检测
│   ├── schemas/          # Zod验证schema
│   └── app.tsx           # 应用入口
├── config/
│   ├── index.ts          # 主配置
│   ├── dev.ts            # 开发配置
│   └── prod.ts           # 生产配置
└── tailwind.config.js    # Tailwind配置
```

## 组件开发规范

### Taro组件适配

**基础组件映射：**
- `View` → `@tarojs/components`
- `Text` → `@tarojs/components`
- `Button` → `@tarojs/components`
- `Input` → `@tarojs/components`

**组件封装示例：**
```typescript
// src/components/ui/button.tsx
import { Button as TaroButton, ButtonProps as TaroButtonProps } from '@tarojs/components'
import { cn } from '@/utils/cn'
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-9 px-3 rounded-md text-xs',
        lg: 'h-11 px-8 rounded-md',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
)

interface ButtonProps extends Omit<TaroButtonProps, 'size'>, VariantProps<typeof buttonVariants> {
  className?: string
  children?: React.ReactNode
}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <TaroButton
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}
```

### 页面组件规范

**页面结构：**
```typescript
// src/pages/index/index.tsx
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { Button } from '@/components/ui/button'

export default function IndexPage() {
  useLoad(() => {
    console.log('Page loaded.')
  })

  return (
    <View className="min-h-screen bg-background">
      <View className="container mx-auto px-4">
        <Text className="text-2xl font-bold">首页</Text>
        <Button variant="default">点击按钮</Button>
      </View>
    </View>
  )
}
```

**页面配置：**
```typescript
// src/pages/index/index.config.ts
export default definePageConfig({
  navigationBarTitleText: '首页',
  enableShareAppMessage: true,
  usingComponents: {}
})
```

## 状态管理规范

### React Query配置
```typescript
// src/utils/auth.tsx
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      staleTime: 5 * 60 * 1000, // 5分钟
    },
  },
})
```

### 数据获取示例
```typescript
import { useQuery } from '@tanstack/react-query'
import { api } from '@/utils/api'

export function useRoutes(params: RouteQueryParams) {
  return useQuery({
    queryKey: ['routes', params],
    queryFn: () => api.routes.list(params),
    enabled: !!params.startPoint && !!params.endPoint,
  })
}
```

## 路由和导航规范

### 页面跳转
```typescript
import { navigateTo, switchTab, redirectTo } from '@tarojs/taro'

// 普通页面跳转
navigateTo({
  url: '/pages/schedule-list/index'
})

// Tab页面跳转
switchTab({
  url: '/pages/index/index'
})

// 重定向
redirectTo({
  url: '/pages/login/index'
})
```

### 路由参数处理
```typescript
import { useRouter } from '@tarojs/taro'

export default function ScheduleListPage() {
  const router = useRouter()
  const { startPoint, endPoint, date } = router.params

  // 参数验证和类型转换
  const queryParams = {
    startPoint: startPoint || '',
    endPoint: endPoint || '',
    date: date ? new Date(date) : new Date()
  }
}
```

## 平台适配规范

### 平台检测
```typescript
// src/utils/platform.ts
export const isWeapp = process.env.TARO_ENV === 'weapp'
export const isH5 = process.env.TARO_ENV === 'h5'
export const isAlipay = process.env.TARO_ENV === 'alipay'

export function usePlatform() {
  return {
    isWeapp,
    isH5,
    isAlipay,
    platform: process.env.TARO_ENV || 'weapp'
  }
}
```

### 条件渲染
```typescript
import { View, Text } from '@tarojs/components'
import { isWeapp, isH5 } from '@/utils/platform'

export function PlatformSpecificComponent() {
  return (
    <View>
      {isWeapp && <Text>微信小程序特有内容</Text>}
      {isH5 && <Text>H5特有内容</Text>}
      <Text>通用内容</Text>
    </View>
  )
}
```

## 小程序API使用规范

### 授权和权限
```typescript
import { authorize, getSetting } from '@tarojs/taro'

export async function requestUserLocation() {
  try {
    const { authSetting } = await getSetting()

    if (!authSetting['scope.userLocation']) {
      await authorize({ scope: 'scope.userLocation' })
    }

    // 获取位置信息
    const { latitude, longitude } = await getLocation()
    return { latitude, longitude }
  } catch (error) {
    console.error('获取位置失败:', error)
    throw error
  }
}
```

### 文件上传
```typescript
import { chooseImage, uploadFile } from '@tarojs/taro'

export async function uploadAvatar() {
  try {
    const { tempFilePaths } = await chooseImage({
      count: 1,
      sizeType: ['compressed'],
      sourceType: ['album', 'camera']
    })

    const uploadResult = await uploadFile({
      url: `${API_BASE}/files/upload`,
      filePath: tempFilePaths[0],
      name: 'file',
      header: {
        'Authorization': `Bearer ${token}`
      }
    })

    return JSON.parse(uploadResult.data)
  } catch (error) {
    console.error('上传失败:', error)
    throw error
  }
}
```

## 性能优化规范

### 图片优化
```typescript
import { Image } from '@tarojs/components'

export function OptimizedImage({ src, alt, className }) {
  return (
    <Image
      src={src}
      mode="aspectFill"
      lazyLoad
      className={className}
      onError={(e) => {
        console.warn('图片加载失败:', src)
        // 设置默认图片
      }}
    />
  )
}
```

### 列表优化
```typescript
import { VirtualList } from '@tarojs/components'

export function RouteList({ routes }) {
  return (
    <VirtualList
      height={500}
      width="100%"
      itemData={routes}
      itemCount={routes.length}
      itemSize={100}
    >
      {({ index, data }) => (
        <RouteItem route={data[index]} />
      )}
    </VirtualList>
  )
}
```

## 错误处理规范

### 全局错误处理
```typescript
// src/app.tsx
import { useEffect } from 'react'
import { useError } from '@tarojs/taro'

export default function App({ children }) {
  useError((error) => {
    console.error('全局错误:', error)
    // 上报错误到监控系统
  })

  return children
}
```

### API错误处理
```typescript
export async function apiCall<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn()
  } catch (error) {
    if (error.status === 401) {
      // 跳转到登录页
      redirectTo({ url: '/pages/login/index' })
    }
    throw error
  }
}
```

## API调用规范

### RPC客户端使用

**基础API调用：**
```typescript
import { userClient } from '@/api'

export async function fetchUsers(params: UserQueryParams) {
  try {
    const response = await userClient.$get({
      query: {
        page: params.page || 1,
        pageSize: params.pageSize || 10,
        keyword: params.keyword,
        sortBy: params.sortBy,
        sortOrder: params.sortOrder
      }
    })

    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`)
    }

    return await response.json()
  } catch (error) {
    console.error('获取用户列表失败:', error)
    throw error
  }
}
```

**带认证的API调用：**
```typescript
import { authClient } from '@/api'

export async function login(username: string, password: string) {
  try {
    const response = await authClient.login.$post({
      json: { username, password }
    })

    if (!response.ok) {
      throw new Error(`登录失败: ${response.status}`)
    }

    const result = await response.json()

    // 存储token
    Taro.setStorageSync('mini_token', result.token)

    return result
  } catch (error) {
    console.error('登录失败:', error)
    throw error
  }
}
```

### 文件上传规范

**使用统一上传接口：**
```typescript
import { uploadFromSelect } from '@/utils/minio'

export async function uploadAvatar() {
  try {
    const result = await uploadFromSelect('avatars', {
      sourceType: ['album'],
      count: 1,
      accept: 'image/*',
      maxSize: 5 * 1024 * 1024 // 5MB
    }, {
      onProgress: (event) => {
        console.log(`上传进度: ${event.progress}%`)
        // 更新UI进度条
      },
      onComplete: () => {
        console.log('上传完成')
        Taro.showToast({ title: '上传成功', icon: 'success' })
      },
      onError: (error) => {
        console.error('上传失败:', error)
        Taro.showToast({ title: '上传失败', icon: 'none' })
      }
    })

    return result.fileUrl
  } catch (error) {
    console.error('文件选择或上传失败:', error)
    throw error
  }
}
```

**手动文件上传：**
```typescript
import { uploadMinIOWithPolicy } from '@/utils/minio'

export async function uploadFileManually(filePath: string, fileName: string) {
  try {
    const result = await uploadMinIOWithPolicy(
      'documents', // 上传路径
      filePath,    // 文件路径（小程序）或File对象（H5）
      fileName,    // 文件名
      {
        onProgress: (event) => {
          console.log(`上传进度: ${event.progress}%`)
        }
      }
    )

    return result
  } catch (error) {
    console.error('文件上传失败:', error)
    throw error
  }
}
```

### 错误处理规范

**统一错误处理：**
```typescript
export const handleApiError = (error: any) => {
  console.error('API错误:', error)

  if (error.status === 401) {
    // 未授权，跳转到登录页
    Taro.redirectTo({ url: '/pages/login/index' })
    Taro.showToast({ title: '请重新登录', icon: 'none' })
  } else if (error.status === 403) {
    // 权限不足
    Taro.showToast({ title: '权限不足', icon: 'none' })
  } else if (error.status === 404) {
    // 资源不存在
    Taro.showToast({ title: '资源不存在', icon: 'none' })
  } else if (error.status >= 500) {
    // 服务器错误
    Taro.showToast({ title: '服务器错误，请稍后重试', icon: 'none' })
  } else {
    // 其他错误
    Taro.showToast({ title: error.message || '网络错误', icon: 'none' })
  }
}
```

**React Query错误处理：**
```typescript
import { useQuery } from '@tanstack/react-query'
import { handleApiError } from '@/utils/error-handler'

export function useUserProfile(userId: number) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await userClient[':id'].$get({ param: { id: userId.toString() } })
      if (!response.ok) {
        throw new Error(`获取用户信息失败: ${response.status}`)
      }
      return response.json()
    },
    onError: handleApiError,
    retry: 1,
    staleTime: 5 * 60 * 1000 // 5分钟缓存
  })
}
```

## 测试规范

### 单元测试
```typescript
// tests/unit/components/button.test.tsx
import { render } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button', () => {
  it('renders with default variant', () => {
    const { getByText } = render(<Button>点击我</Button>)
    expect(getByText('点击我')).toBeInTheDocument()
  })
})
```

### API测试
```typescript
// tests/unit/api/user-api.test.ts
import { userClient } from '@/api'

describe('User API', () => {
  it('should fetch user list', async () => {
    const response = await userClient.$get({
      query: { page: 1, pageSize: 10 }
    })

    expect(response.ok).toBe(true)
    const data = await response.json()
    expect(data).toHaveProperty('data')
    expect(data).toHaveProperty('pagination')
  })
})
```

### E2E测试
```typescript
// tests/e2e/travel-flow.spec.ts
test('完整的出行查询流程', async ({ page }) => {
  await page.goto('/pages/index/index')
  await page.fill('[data-testid="start-point"]', '北京')
  await page.fill('[data-testid="end-point"]', '上海')
  await page.click('[data-testid="search-button"]')

  await expect(page.locator('[data-testid="route-list"]')).toBeVisible()
})
```

## 部署和发布规范

### 构建命令
```bash
# 微信小程序开发环境
pnpm dev:weapp

# 微信小程序生产构建
pnpm build:weapp

# H5开发环境
pnpm dev:h5

# H5生产构建
pnpm build:h5
```

### 版本管理
- 使用语义化版本控制
- 每次发布前更新版本号
- 维护CHANGELOG.md记录变更

## 最佳实践总结

1. **组件设计**: 优先使用函数组件和Hooks
2. **状态管理**: 使用React Query管理服务端状态
3. **样式处理**: 统一使用Tailwind CSS + shadcn/ui
4. **类型安全**: 全面使用TypeScript
5. **错误处理**: 统一的错误处理机制
6. **性能优化**: 合理使用懒加载和虚拟列表
7. **测试覆盖**: 编写全面的单元测试和E2E测试

---

**文档状态**: 正式版
**下次评审**: 2025-11-15