Ver Fonte

docs(standards): 更新Mini UI包规范,添加React Query数据获取规范

新增内容:
- 第9章: 数据获取规范
  * 使用React Query管理服务端状态
  * 基本用法示例
  * 多个独立查询示例
  * 错误处理模式

- 第10章: 测试规范更新
  * 添加页面集成测试示例(使用真实React Query)
  * 测试配置更新

- 第11章: 常见问题新增React Query问题
  * QueryClientProvider包裹
  * queryKey冲突解决

- 第12章: 最佳实践新增
  * 数据获取最佳实践
  * React Query使用规范

技术要点:
- 必须使用useQuery而不是useState + useEffect
- 测试中必须使用真实的QueryClientProvider
- 为每个query使用唯一的queryKey

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname há 3 semanas atrás
pai
commit
ff0621d0ac
1 ficheiros alterados com 240 adições e 14 exclusões
  1. 240 14
      docs/architecture/mini-ui-package-standards.md

+ 240 - 14
docs/architecture/mini-ui-package-standards.md

@@ -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包的开发都应遵循本规范