# 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, VariantProps { className?: string children?: React.ReactNode } export function Button({ className, variant, size, ...props }: ButtonProps) { return ( ) } ``` ### 页面组件规范 **页面结构:** ```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 ( 首页 ) } ``` **页面配置:** ```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 ( {isWeapp && 微信小程序特有内容} {isH5 && H5特有内容} 通用内容 ) } ``` ## 小程序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 ( { console.warn('图片加载失败:', src) // 设置默认图片 }} /> ) } ``` ### 列表优化 ```typescript import { VirtualList } from '@tarojs/components' export function RouteList({ routes }) { return ( {({ index, data }) => ( )} ) } ``` ## 错误处理规范 ### 全局错误处理 ```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(fn: () => Promise): Promise { 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() 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