| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-10-15 | 初始Taro小程序开发规范 | Winston |
本文档定义了出行服务项目中Taro小程序开发的规范和最佳实践,确保代码质量、一致性和可维护性。
{
"@tarojs/taro": "4.1.4",
"@tarojs/react": "4.1.4",
"react": "^18.0.0",
"tailwindcss": "^4.1.11",
"weapp-tailwindcss": "^4.2.5"
}
// 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配置
基础组件映射:
View → @tarojs/componentsText → @tarojs/componentsButton → @tarojs/componentsInput → @tarojs/components组件封装示例:
// 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}
/>
)
}
页面结构:
// 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>
)
}
页面配置:
// src/pages/index/index.config.ts
export default definePageConfig({
navigationBarTitleText: '首页',
enableShareAppMessage: true,
usingComponents: {}
})
// src/utils/auth.tsx
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
staleTime: 5 * 60 * 1000, // 5分钟
},
},
})
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,
})
}
import { navigateTo, switchTab, redirectTo } from '@tarojs/taro'
// 普通页面跳转
navigateTo({
url: '/pages/schedule-list/index'
})
// Tab页面跳转
switchTab({
url: '/pages/index/index'
})
// 重定向
redirectTo({
url: '/pages/login/index'
})
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()
}
}
// 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'
}
}
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>
)
}
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
}
}
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
}
}
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)
// 设置默认图片
}}
/>
)
}
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>
)
}
// src/app.tsx
import { useEffect } from 'react'
import { useError } from '@tarojs/taro'
export default function App({ children }) {
useError((error) => {
console.error('全局错误:', error)
// 上报错误到监控系统
})
return children
}
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调用:
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调用:
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
}
}
使用统一上传接口:
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
}
}
手动文件上传:
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
}
}
统一错误处理:
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错误处理:
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分钟缓存
})
}
// 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()
})
})
// 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')
})
})
// 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()
})
# 微信小程序开发环境
pnpm dev:weapp
# 微信小程序生产构建
pnpm build:weapp
# H5开发环境
pnpm dev:h5
# H5生产构建
pnpm build:h5
文档状态: 正式版 下次评审: 2025-11-15