|
|
@@ -741,16 +741,150 @@ mini-ui-packages/<package-name>/
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+## 数据获取规范
|
|
|
+
|
|
|
+### 9.1 使用React Query管理服务端状态
|
|
|
+
|
|
|
+**重要**: Mini UI包必须使用React Query (`@tanstack/react-query`) 管理服务端状态,而不是手动使用`useState` + `useEffect`。
|
|
|
+
|
|
|
+**原因**:
|
|
|
+- 符合项目技术栈要求(见`component-architecture.md`)
|
|
|
+- 自动处理加载状态、错误状态、缓存
|
|
|
+- 更好的类型推断和RPC集成
|
|
|
+- 统一的数据获取模式
|
|
|
+
|
|
|
+#### 9.1.1 基本用法
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { useQuery } from '@tanstack/react-query'
|
|
|
+import { apiClient } from '../api'
|
|
|
+
|
|
|
+const MyPage: React.FC = () => {
|
|
|
+ // ✅ 正确: 使用React Query
|
|
|
+ const { data, isLoading, error } = useQuery({
|
|
|
+ queryKey: ['resource-name'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await apiClient.resource.$get()
|
|
|
+ if (!res.ok) {
|
|
|
+ throw new Error('获取数据失败')
|
|
|
+ }
|
|
|
+ return await res.json()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // ❌ 错误: 不要使用useState + useEffect手动管理状态
|
|
|
+ // const [data, setData] = useState(null)
|
|
|
+ // const [loading, setLoading] = useState(true)
|
|
|
+ // useEffect(() => {
|
|
|
+ // const fetchData = async () => {
|
|
|
+ // setLoading(true)
|
|
|
+ // const res = await apiClient.resource.$get()
|
|
|
+ // const data = await res.json()
|
|
|
+ // setData(data)
|
|
|
+ // setLoading(false)
|
|
|
+ // }
|
|
|
+ // fetchData()
|
|
|
+ // }, [])
|
|
|
+
|
|
|
+ if (isLoading) return <div>加载中...</div>
|
|
|
+ if (error) return <div>加载失败</div>
|
|
|
+
|
|
|
+ return <div>{/* 渲染数据 */}</div>
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 9.1.2 多个独立查询
|
|
|
+
|
|
|
+```typescript
|
|
|
+const MyPage: React.FC = () => {
|
|
|
+ // 多个独立的查询可以并行执行
|
|
|
+ const { data: statusData, isLoading: statusLoading } = useQuery({
|
|
|
+ queryKey: ['employment-status'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await apiClient.employment.status.$get()
|
|
|
+ if (!res.ok) throw new Error('获取就业状态失败')
|
|
|
+ return await res.json()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const { data: recordsData, isLoading: recordsLoading } = useQuery({
|
|
|
+ queryKey: ['salary-records'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await apiClient.employment['salary-records'].$get({
|
|
|
+ query: { take: 3 }
|
|
|
+ })
|
|
|
+ if (!res.ok) throw new Error('获取薪资记录失败')
|
|
|
+ const data = await res.json()
|
|
|
+ return data.data || []
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const { data: historyData, isLoading: historyLoading } = useQuery({
|
|
|
+ queryKey: ['employment-history'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await apiClient.employment.history.$get({
|
|
|
+ query: { take: 20 }
|
|
|
+ })
|
|
|
+ if (!res.ok) throw new Error('获取就业历史失败')
|
|
|
+ const data = await res.json()
|
|
|
+ return data.data || []
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 每个查询有独立的loading状态
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <StatusCard data={statusData} loading={statusLoading} />
|
|
|
+ <RecordsCard data={recordsData} loading={recordsLoading} />
|
|
|
+ <HistoryCard data={historyData} loading={historyLoading} />
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 9.1.3 错误处理
|
|
|
+
|
|
|
+```typescript
|
|
|
+const MyPage: React.FC = () => {
|
|
|
+ const { data, isLoading, error } = useQuery({
|
|
|
+ queryKey: ['resource'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await apiClient.resource.$get()
|
|
|
+ if (!res.ok) {
|
|
|
+ throw new Error('获取数据失败')
|
|
|
+ }
|
|
|
+ return await res.json()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 使用useEffect处理错误
|
|
|
+ React.useEffect(() => {
|
|
|
+ if (error) {
|
|
|
+ Taro.showToast({
|
|
|
+ title: error.message,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, [error])
|
|
|
+
|
|
|
+ if (isLoading) return <div>加载中...</div>
|
|
|
+
|
|
|
+ return <div>{/* 渲染数据 */}</div>
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
## 测试规范
|
|
|
|
|
|
-### 9.1 Jest配置
|
|
|
+### 10.1 Jest配置
|
|
|
|
|
|
```javascript
|
|
|
module.exports = {
|
|
|
preset: 'ts-jest',
|
|
|
testEnvironment: 'jsdom',
|
|
|
- setupFilesAfterEnv: ['@d8d/mini-testing-utils/setup'],
|
|
|
+ setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],
|
|
|
moduleNameMapper: {
|
|
|
+ '^@/(.*)$': '<rootDir>/src/$1',
|
|
|
+ '^~/(.*)$': '<rootDir>/tests/$1',
|
|
|
'^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
|
|
|
'\\.(css|less|scss)$': '@d8d/mini-testing-utils/testing/style-mock.js'
|
|
|
},
|
|
|
@@ -763,7 +897,7 @@ module.exports = {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### 9.2 组件测试示例
|
|
|
+### 10.2 组件测试示例
|
|
|
|
|
|
```typescript
|
|
|
import { render, screen } from '@testing-library/react'
|
|
|
@@ -781,9 +915,76 @@ describe('MyComponent', () => {
|
|
|
})
|
|
|
```
|
|
|
|
|
|
+### 10.3 页面集成测试(使用React Query)
|
|
|
+
|
|
|
+**重要**: 页面集成测试必须使用真实的React Query,不要mock React Query。
|
|
|
+
|
|
|
+```typescript
|
|
|
+import React from 'react'
|
|
|
+import { render, screen, waitFor } from '@testing-library/react'
|
|
|
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
+import MyPage from '../pages/MyPage'
|
|
|
+
|
|
|
+// Mock API client
|
|
|
+jest.mock('../api', () => ({
|
|
|
+ apiClient: {
|
|
|
+ resource: {
|
|
|
+ $get: jest.fn()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}))
|
|
|
+
|
|
|
+const { apiClient } = require('../api')
|
|
|
+
|
|
|
+const createTestQueryClient = () => new QueryClient({
|
|
|
+ defaultOptions: {
|
|
|
+ queries: { retry: false, staleTime: Infinity },
|
|
|
+ mutations: { retry: false }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const renderWithQueryClient = (component: React.ReactElement) => {
|
|
|
+ const queryClient = createTestQueryClient()
|
|
|
+ return render(
|
|
|
+ <QueryClientProvider client={queryClient}>
|
|
|
+ {component}
|
|
|
+ </QueryClientProvider>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+describe('MyPage', () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ jest.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('应该显示加载状态', async () => {
|
|
|
+ // Mock API为pending状态
|
|
|
+ apiClient.resource.$get.mockImplementation(() => new Promise(() => {}))
|
|
|
+
|
|
|
+ renderWithQueryClient(<MyPage />)
|
|
|
+
|
|
|
+ expect(screen.getByText('加载中...')).toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('应该成功加载并显示数据', async () => {
|
|
|
+ const mockData = { name: '测试数据' }
|
|
|
+ apiClient.resource.$get.mockResolvedValue({
|
|
|
+ ok: true,
|
|
|
+ json: async () => mockData
|
|
|
+ })
|
|
|
+
|
|
|
+ renderWithQueryClient(<MyPage />)
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByText('测试数据')).toBeInTheDocument()
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
## 常见问题和解决方案
|
|
|
|
|
|
-### 10.1 布局问题
|
|
|
+### 11.1 布局问题
|
|
|
|
|
|
**问题**: 元素横向排列而不是垂直排列
|
|
|
- **原因**: View容器默认是flex-row
|
|
|
@@ -793,7 +994,7 @@ describe('MyComponent', () => {
|
|
|
- **原因**: Text组件默认是内联显示
|
|
|
- **解决**: 父容器添加`flex flex-col`类
|
|
|
|
|
|
-### 10.2 样式问题
|
|
|
+### 11.2 样式问题
|
|
|
|
|
|
**问题**: Tailwind样式不生效
|
|
|
- **原因**: 类名冲突或拼写错误
|
|
|
@@ -803,30 +1004,51 @@ describe('MyComponent', () => {
|
|
|
- **原因**: 不同小程序平台的样式引擎差异
|
|
|
- **解决**: 使用Taro提供的跨平台样式方案,避免使用平台特有样式
|
|
|
|
|
|
-### 10.3 API问题
|
|
|
+### 11.3 API问题
|
|
|
|
|
|
**问题**: RPC客户端类型错误
|
|
|
- **原因**: API路径映射错误或类型推断不正确
|
|
|
- **解决**: 验证后端路由定义,使用RPC推断类型
|
|
|
|
|
|
+### 11.4 React Query问题
|
|
|
+
|
|
|
+**问题**: 测试中React Query不工作
|
|
|
+- **原因**: 忘记使用QueryClientProvider包裹
|
|
|
+- **解决**: 使用renderWithQueryClient包装组件
|
|
|
+
|
|
|
+**问题**: queryKey冲突导致数据混乱
|
|
|
+- **原因**: 不同查询使用了相同的queryKey
|
|
|
+- **解决**: 为每个查询使用唯一的queryKey
|
|
|
+
|
|
|
## 最佳实践
|
|
|
|
|
|
-### 11.1 组件开发
|
|
|
+### 12.1 组件开发
|
|
|
|
|
|
1. **始终使用flex flex-col实现垂直布局**
|
|
|
2. **为每个View添加语义化的className**
|
|
|
3. **使用data-testid属性便于测试**
|
|
|
4. **组件props使用TypeScript接口定义**
|
|
|
5. **使用相对路径导入包内模块**
|
|
|
+6. **使用React Query管理服务端状态**
|
|
|
+7. **为每个query使用唯一的queryKey**
|
|
|
|
|
|
-### 11.2 性能优化
|
|
|
+### 12.2 数据获取
|
|
|
+
|
|
|
+1. **使用useQuery获取数据,不要使用useState + useEffect**
|
|
|
+2. **在queryFn中检查response.ok,失败时throw Error**
|
|
|
+3. **使用useEffect处理错误,显示Toast提示**
|
|
|
+4. **多个独立查询使用不同的queryKey**
|
|
|
+5. **测试中使用真实的QueryClientProvider**
|
|
|
+
|
|
|
+### 12.3 性能优化
|
|
|
|
|
|
1. **使用Image组件的lazyLoad属性**
|
|
|
2. **列表数据使用虚拟滚动**
|
|
|
3. **避免不必要的重渲染**
|
|
|
4. **使用React.memo优化组件性能**
|
|
|
+5. **利用React Query的缓存机制减少重复请求**
|
|
|
|
|
|
-### 11.3 代码质量
|
|
|
+### 12.4 代码质量
|
|
|
|
|
|
1. **遵循项目编码标准**
|
|
|
2. **编写单元测试和集成测试**
|
|
|
@@ -836,19 +1058,20 @@ describe('MyComponent', () => {
|
|
|
|
|
|
## 参考实现
|
|
|
|
|
|
-### 12.1 用人方小程序UI包
|
|
|
+### 13.1 用人方小程序UI包
|
|
|
|
|
|
- `mini-ui-packages/yongren-dashboard-ui`
|
|
|
- `mini-ui-packages/yongren-order-management-ui`
|
|
|
- `mini-ui-packages/yongren-talent-management-ui`
|
|
|
|
|
|
-### 12.2 人才小程序UI包
|
|
|
+### 13.2 人才小程序UI包
|
|
|
|
|
|
- `mini-ui-packages/rencai-dashboard-ui`
|
|
|
- `mini-ui-packages/rencai-personal-info-ui`
|
|
|
+- `mini-ui-packages/rencai-employment-ui` - 使用React Query的参考实现
|
|
|
- `mini-ui-packages/rencai-auth-ui`
|
|
|
|
|
|
-### 12.3 共享组件
|
|
|
+### 13.3 共享组件
|
|
|
|
|
|
- `mini-ui-packages/mini-shared-ui-components`
|
|
|
|
|
|
@@ -856,11 +1079,14 @@ describe('MyComponent', () => {
|
|
|
|
|
|
| 版本 | 日期 | 变更说明 | 作者 |
|
|
|
|------|------|----------|------|
|
|
|
-| 1.0 | 2025-12-26 | 初始版本,基于史诗011和017经验总结 | Bob (Scrum Master) |
|
|
|
+| 1.2 | 2025-12-28 | 添加React Query数据获取规范,更新测试规范章节 | James (Claude Code) |
|
|
|
+| 1.1 | 2025-12-26 | 添加图标使用规范(Heroicons) | Bob (Scrum Master) |
|
|
|
+| 1.0 | 2025-12-26 | 基于史诗011和017经验创建Mini UI包开发规范 | Bob (Scrum Master) |
|
|
|
|
|
|
---
|
|
|
|
|
|
**重要提醒**:
|
|
|
1. 本规范专门针对Taro小程序UI包开发,与Web UI包开发规范(`ui-package-standards.md`)不同
|
|
|
2. `flex flex-col`是Taro小程序中最常用的布局类,请务必牢记
|
|
|
-3. 所有Mini UI包的开发都应遵循本规范
|
|
|
+3. **使用React Query管理服务端状态**,不要使用useState + useEffect手动管理
|
|
|
+4. 所有Mini UI包的开发都应遵循本规范
|