Piko Frontend Code Standards
严格派:代码即文档,每一行都有存在的理由。
Core Philosophy
- Code as Documentation — 代码即文档,命名自解释,拒绝冗余注释
- Page Autonomy — 每个页面是自包含单元,拥有自己的 components/hooks/types/consts/utils
- Single Responsibility — 一个函数/组件/hook 只做一件事
- Composition over Complexity — Slot 组合 > 巨型组件,Hook 组合 > 万能 Hook
- Explicit over Implicit — 所有返回类型显式标注,所有 Promise 要么 await 要么 void
- Type as Contract — 类型系统是模块间的契约,用判别联合而非松散可选字段
- Fail Fast, Fail Loud — 纯函数映射错误类型,不在 hook 里硬编码错误逻辑
Comments: Less is More
MUST: 用清晰的函数名/变量名/类型名替代注释
MUST: 复杂算法或非直觉的业务规则才需要注释
MUST: 注释解释 WHY,不解释 WHAT (代码本身说明 what)
NEVER: 函数签名上方加 JSDoc 复述参数名和类型 — TypeScript 已经表达了
NEVER: 注释掉的代码 — 直接删除,git 有历史
NEVER: 分隔线注释 (// ────────) — 用空行和函数拆分表达结构
NEVER: "显而易见"的注释 (// 设置 loading 为 true → setIsLoading(true))
Architecture: Page-Autonomous Structure
frontend/
├── app/ # Expo Router 路由 (Screen 层)
│ ├── _layout.tsx
│ ├── (tabs)/
│ │ ├── index.tsx # → 编排 pages/home 的内容
│ │ ├── ai/index.tsx
│ │ └── profile/index.tsx
│ └── chat/[id].tsx
│
├── pages/ # 页面业务单元 (每个页面自包含)
│ ├── home/
│ │ ├── components/ # 页面私有组件
│ │ ├── hooks/ # 页面私有 hooks
│ │ ├── types/ # 页面私有类型
│ │ ├── consts/ # 页面私有常量
│ │ └── utils/ # 页面私有工具函数
│ ├── profile/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── types/
│ └── chat/
│ ├── components/
│ ├── hooks/
│ └── types/
│
├── common/ # 跨页面共享
│ ├── components/
│ │ ├── page-loading/ # 通用加载
│ │ ├── page-status-view/ # 通用错误/空态 + getPageErrorType()
│ │ ├── biz/ # 业务级共享组件
│ │ └── product-card/ # 可复用卡片 (Slot 组合)
│ ├── hooks/ # 通用 hooks (useAuth, useSafeArea)
│ │ └── index.ts # Barrel re-export
│ ├── typings/ # 共享类型定义
│ └── consts/ # 全局常量
│
├── services/ # API client + 页面级数据获取
│ ├── api-client.ts # 底层请求封装 (post, postSafe, postDirect)
│ ├── home.ts # 首页数据获取
│ ├── chat.ts # 聊天数据获取
│ ├── profile.ts # 个人资料数据获取
│ └── telegram.ts # Telegram 认证 & 消息 API
│
├── lib/ # 框架级配置(如认证客户端)
│ └── auth-client.ts # Better Auth 客户端 (createAuthClient),session/Cookie 与后端一致
│
├── contexts/ # Context 定义
└── utils/ # 全局工具函数
关键原则
MUST: 页面私有代码放 pages/{page}/ 下,不放 common/
MUST: 跨 2 个以上页面复用的代码提升到 common/
MUST: 数据获取逻辑放 services/ (按页面分文件),不内联在组件中
MUST: common/hooks/index.ts barrel re-export 所有公共 hooks
MUST: 页面私有工具函数放 pages/{page}/utils/,一个文件一个主函数
MUST: utils/ 文件名 = 主函数名 (kebab-case),如 calc-column-widths.ts
MUST: utils/index.ts barrel re-export 所有工具函数
MUST: utils/ 仅含纯函数 — 不含 hooks / 组件 / 副作用
NEVER: 工具函数内联在组件文件中 — 提取到 utils/
NEVER: 页面级 hooks/components 目录创建 barrel index.ts — 直接 import 具体文件
NEVER: 页面 A 直接 import 页面 B 的私有模块
NEVER: common/ 下的代码 import pages/ 下的代码 (依赖方向: pages → common)
Component Patterns
分类与位置
| 类型 | 位置 | 职责 |
|---|---|---|
| Screen | app/ | 路由入口,编排页面组件 |
| Page Component | pages/{page}/components/ | 页面私有 UI |
| Biz Shared | common/components/biz/ | 跨页面业务组件 |
| Base Shared | common/components/ | 通用 UI,零业务 |
规则
MUST: Props 接口命名: 文件内用 Props,跨文件导出用 {ComponentName}Props
MUST: 显式返回类型: (props: Props): ReactNode => { ... }
MUST: 页面内组件用页面前缀: Chat 页的组件用 Chat 前缀 (ChatBubble, ChatInput)
MUST: 条件渲染统一三元: {condition ? <X /> : null}
MUST: 空状态统一 return null
MUST: Slot 组合: 通过 leftArea/title/footer 等 ReactNode props 组合复杂布局
MUST: 多组件文件 — 主组件 index.tsx + 子组件各自独立文件,放同名文件夹下
MUST: 子组件文件名 = 组件名 (kebab-case),如 waiting-indicator.tsx
ALLOW: 极简渲染辅助 (无 hooks、1-3 行 JSX) — 可用箭头函数常量留在同文件
MUST: 组件文件内的非公用辅助函数使用箭头函数常量 (const fn = (...): T => { ... }),不用 function 声明
NEVER: function 声明式定义多个组件在同一文件
NEVER: 超过 150 行的组件 — 拆分,依据具体情况拆分,如果真的要这么多,那就不拆了
NEVER: Props 透传超过 2 层 — 用 Context
Slot 组合示例
typescript1interface Props<T> { 2 data: T; 3 leftArea?: ReactNode; 4 title?: ReactNode; 5 subtitle?: ReactNode; 6 operationArea?: ReactNode; 7 onPress?: (data: T) => void; 8} 9 10export default function CardContainer<T>({ data, leftArea, title, subtitle, operationArea, onPress }: Props<T>): ReactNode { 11 return ( 12 <XStack onPress={() => onPress?.(data)}> 13 {leftArea ? <YStack flexShrink={0}>{leftArea}</YStack> : null} 14 <YStack flex={1}> 15 {title ? title : null} 16 {subtitle ? subtitle : null} 17 {operationArea ? operationArea : null} 18 </YStack> 19 </XStack> 20 ); 21}
Hook Taxonomy
前置规则: 禁止空壳 re-export
NEVER: 创建只做 re-export 的 hook 文件 (如 export { useX } from 'lib')
→ 直接从源库导入,空壳文件是死代码的温床
ONLY: 当你封装了自定义逻辑时,才值得创建 hook 文件
useCallback / useMemo 使用规则
NEVER: 默认给事件处理函数套 useCallback,给计算结果套 useMemo — 大多数场景没有收益
ONLY: useCallback: 回调传给 React.memo 子组件,或作为 useEffect/useMemo 的依赖
ONLY: useMemo: 计算确实昂贵 (大数组遍历、复杂格式化),或结果作为 useEffect 的依赖需要稳定引用
MUST: 简单赋值/三元/条件判断直接写,不套 useMemo
MUST: 组件内部的事件处理函数直接写普通函数,不套 useCallback
Hooks 严格分三类,每类有明确的约束:
① 数据 Hook (Data Hook)
职责:获取数据 + 管理 loading/error 状态。返回结构化对象。
MUST: 返回命名字段: { isLoading, errorType, data, handleRetry, handleRefresh }
MUST: 错误映射使用纯函数 getPageErrorType(),不在 hook 中硬编码
MUST: 最多 1 个 useEffect (初始加载)
MUST: 返回类型显式定义为 interface
MUST: 使用 state trigger (fetchKey) + useEffect 重新触发请求,不用 useCallback 包裹请求逻辑
MUST: useEffect cleanup 设置 cancelled 标记防止竞态更新
NEVER: useCallback 包裹网络请求函数 — 请求只在 useEffect 内发起
标准 Data Hook 模式
typescript1// 简单模式: 加载 + 重试 2const [fetchKey, setFetchKey] = useState<number>(0); 3 4useEffect(() => { 5 let cancelled = false; 6 setIsLoading(true); 7 setErrorType(undefined); 8 9 async function load(): Promise<void> { 10 try { 11 const response = await fetchXxxPage(); 12 if (cancelled) return; 13 const mappedError = getPageErrorType(response); 14 if (mappedError) { 15 setErrorType(mappedError); 16 setData(null); 17 } else { 18 setData(response.data ?? null); 19 } 20 } catch { 21 if (cancelled) return; 22 setErrorType(PageErrorType.NETWORK); 23 setData(null); 24 } finally { 25 if (!cancelled) setIsLoading(false); 26 } 27 } 28 29 void load(); 30 return () => { 31 cancelled = true; 32 }; 33}, [fetchKey]); // + 其他依赖如 session 34 35const handleRetry = (): void => { 36 setFetchKey((k) => k + 1); 37};
② 派生 Hook (Derived Hook)
职责:纯计算/数据转换,零副作用。只用 useMemo。
MUST: 只使用 useMemo,不使用 useEffect / useState
MUST: 纯函数语义: 相同输入永远相同输出
MUST: 命名体现数据来源: useDataFromQuery, useFormattedPrice
③ 副作用 Hook (Effect Hook)
职责:管理单一副作用 (事件监听、定时器、性能埋点)。
MUST: 只有 1 个 useEffect
MUST: useRef 保存最新回调 (防止闭包过期)
MUST: cleanup 函数清理所有副作用
MUST: 返回类型为 void (不返回状态)
组合
typescript1// pages/chat/hooks/useChatPageData.ts — 数据 hook (单一 effect) 2// pages/chat/hooks/useChatPolling.ts — 副作用 hook (单一 effect) 3// 在 Screen 层组合: 4const pageData = useChatPageData(chatId); 5useChatPolling(pageData.silentRefresh, pollingInterval);
详见 STANDARDS.md 和 PATTERNS.md
Type Safety
MUST: 所有函数参数 + 返回值显式标注类型
MUST: 组件返回类型标注 ReactNode
MUST: Hook 返回类型定义为 interface (不用内联对象类型)
MUST: Context 用判别联合 (discriminated union) 区分场景
MUST: error 使用 enum PageErrorType,不用 string
MUST: API 响应用 type guard 验证,不用 as T
MUST: 不等待的 Promise 用 void 标记: void doSomething()
NEVER: any — 用 unknown + type guard
NEVER: as T 类型断言
NEVER: ! 非空断言
API/IDL 接口类型(蛇形命名)
凡用于描述接口契约的类型(HTTP 请求体、响应体、SSE 事件 payload 等),其字段名必须使用 snake_case,与后端、第三方 API 的 JSON 契约保持一致,便于跨语言/跨端对齐。
MUST: 请求体/响应体/SSE 事件等 IDL 类型的属性名使用 snake_case (如 conversation_id, header_title)
MUST: 前端读取 API 响应、组装请求体、解析 SSE 时使用与类型一致的 snake_case 键
NEVER: 在接口契约类型中使用 camelCase 字段名
仅在前端或后端内部使用的类型(如组件 Props、Hook 返回类型、内部状态)仍按「代码」命名规范使用 camelCase。
判别联合 Context 示例
typescript1interface ChatDirectContext { 2 scene: 'direct'; 3 peerId: string; 4 getLogParams: () => DirectLogParams; 5} 6 7interface ChatGroupContext { 8 scene: 'group'; 9 groupId: string; 10 memberCount: number; 11 getLogParams: () => GroupLogParams; 12} 13 14type ChatPageContext = ChatDirectContext | ChatGroupContext;
Error Handling
MUST: 纯函数映射错误: getPageErrorType(response) => PageErrorType | undefined
MUST: PageErrorType 使用 enum (DEFAULT, NETWORK, AUTH, EMPTY)
MUST: 数据 Hook 中调用 getPageErrorType 设置错误状态
MUST: 所有 async 必须 try/catch 或 .catch()
MUST: 不等待的 async 调用加 void 前缀: void fetchData()
NEVER: catch(e) {} 空 catch
NEVER: silentLoad 无 catch — 静默操作也要 console.error
标准错误流
API 响应 → getPageErrorType(resp) → PageErrorType | undefined
↓
undefined = 成功,继续处理
PageErrorType = 设置错误状态
↓
Screen: <PageStatusView errorType={errorType} onRetry={handleRetry} />
Style System: Tamagui-First
核心规则
MUST: 使用 Tamagui 组件 (XStack, YStack, View, Text) 替代 RN 原生组件
MUST: 布局用 Tamagui props (bg, px, py, gap, flex, borderRadius, mt, mb, pl 等)
MUST: 颜色只用 theme tokens ($color, $blue9, $gray4, $background)
MUST: 间距只用 size tokens ($1, $2, $3, $4)
MUST: 交互用 Tamagui 的 pressStyle + onPress,不用 RN Pressable/TouchableOpacity
MUST: 非 Tamagui 组件 (如 Ionicons) 的颜色用 useTheme() + theme.xxx.val
NEVER: 从 'react-native' 导入 View / Text — 从 'tamagui' 导入
NEVER: useMemo 构造 style 对象 — 把样式直接写成 Tamagui props
NEVER: className / Tailwind
NEVER: 硬编码颜色 (#ffffff, rgba(...))
ALLOW: style prop — 仅 Tamagui 不支持的属性 (borderRadius 部分值、alignItems 等)
正确 vs 错误示例
typescript1// BAD: RN 组件 + useMemo style 对象 2import { View, Text, Pressable } from 'react-native'; 3const styles = useMemo(() => ({ 4 container: { padding: 12, backgroundColor: theme.blue2.val, borderRadius: 12 }, 5 title: { fontSize: 14, fontWeight: '600', color: theme.blue11.val }, 6}), [themeName]); 7return ( 8 <Pressable onPress={handlePress}> 9 <View style={styles.container}> 10 <Text style={styles.title}>标题</Text> 11 </View> 12 </Pressable> 13); 14 15// GOOD: Tamagui 组件 + inline props 16import { YStack, Text } from 'tamagui'; 17return ( 18 <YStack 19 bg="$blue2" 20 px="$3" 21 py="$2.5" 22 borderWidth={1} 23 borderColor="$blue7" 24 pressStyle={{ opacity: 0.8 }} 25 onPress={handlePress} 26 style={{ borderRadius: 12 }} 27 > 28 <Text fontSize={14} fontWeight="600" color="$blue11">标题</Text> 29 </YStack> 30);
Naming Conventions
文件
组件: kebab-case.tsx (chat-bubble.tsx, page-loading.tsx)
Hook: camelCase.ts (useFetchData.ts, usePolling.ts)
Service: camelCase.ts (chatService.ts, profileService.ts)
Type: index.ts (在 types/ 目录下)
常量: index.ts (在 consts/ 目录下)
工具函数: kebab-case.ts (calc-column-widths.ts, sanitize-streaming-markdown.ts)
目录: kebab-case/ (page-status-view/, operation-button/)
Barrel: common/hooks/index.ts 和 pages/{page}/utils/index.ts 需要 barrel re-export
代码
组件名: PascalCase + 页面前缀 ChatBubble, ChatInput, ProfileCard
Hook: use 前缀 useFetchData, usePolling, useAuth
常量: UPPER_SNAKE_CASE TAB_BAR_HEIGHT, POLLING_INTERVAL
Enum: PascalCase PageErrorType, ChatScene
函数: camelCase + 动词 fetchChatList, getPageErrorType, handleRetry
布尔: is/has/should 前缀 isLoading, hasMedia, shouldRefresh
回调 Props: on 前缀 onPress, onRetry, onBind
处理函数: handle 前缀 handleBind, handleRetry
返回类型: 显式标注 (): ReactNode, (): void, (): Promise<void>
Import Organization
typescript1// 1. React / React Native 核心 2import { useState, useCallback } from 'react'; 3import { Alert } from 'react-native'; 4 5// 2. 第三方框架 6import { useRouter } from 'expo-router'; 7import { YStack, Text } from 'tamagui'; 8 9// 3. 项目 common/ (shared) 10import type { ProfilePageData } from '@/common/typings/profile'; 11import { useAuth } from '@/common/hooks'; 12import PageLoading from '@/common/components/page-loading'; 13import { getPageErrorType } from '@/common/components/page-status-view'; 14 15// 4. 项目 services/ (数据获取) 16import { fetchProfilePage } from '@/services/profile'; 17 18// 5. 页面相对路径 (同页面内) 19import { POLLING_INTERVAL } from '../consts'; 20import type { ChatLogParams } from '../types'; 21import ChatBubble from './chat-bubble';
MUST: type-only imports 使用 import type { X }
MUST: 组间保留空行
MUST: 同组内按字母排序
MUST: common/hooks 用 barrel import: from '@/common/hooks'
认证 (Better Auth)
Piko 统一使用 Better Auth 做登录与鉴权。
MUST: 登录、session、OAuth(如 Apple 登录)使用 frontend/lib/auth-client.ts 的 createAuthClient 实例
MUST: 需要带登录态请求时,用 authClient.getSession() / getCookie() 等注入;若在 fetch 里手动设置 Cookie,使用 credentials: 'omit'(与 Better Auth 文档一致)
MUST: 鉴权/登录相关逻辑以 Better Auth 文档与项目 PRD(如 docs/PRD-Apple-Login-Better-Auth-Alignment.md)为准
NEVER: 引入或教学其他认证库(如 Next-Auth)替代 Better Auth
依赖管理 (Dependency Management)
NEVER: 通过直接编辑 package.json 添加或删除依赖
MUST: 使用包管理器官方命令添加/删除依赖(与本项目一致)
例如: pnpm add <pkg> / pnpm add -D <pkg>、pnpm remove <pkg>
或: npm install <pkg>、yarn add <pkg>(以项目根目录 package.json 或文档为准)
MUST: 添加依赖前确认当前项目使用的包管理器(看 lockfile: pnpm-lock.yaml / package-lock.json / yarn.lock)
无论何时需要新增或移除依赖,都必须通过终端执行官方命令,不得手改 package.json。
Enforcement
写代码时自动检查:
- 页面私有代码是否放在 pages/{page}/ 下?
- 组件是否超过 150 行?→ 拆分,依据具体情况拆分,如果真的要这么多,那就不拆了
- Hook 是否属于明确的分类 (数据/派生/副作用)?
- Hook 是否有多个 useEffect?→ 拆分
- 返回类型是否显式标注?→ 补充 ReactNode / void
- 是否有 any/as/! ?→ type guard
- error 是否用 enum?→ 不用 string
- 不等待的 Promise 是否加了 void?
- 样式是否用 Tamagui token?→ 不用 className/硬编码
- import 是否按规范分组?
- 新增/删除依赖是否通过包管理器命令?→ 禁止直接编辑 package.json
- 登录/鉴权相关是否使用 Better Auth(auth-client、credentials: 'omit' 当手动设 Cookie)?→ 见「认证 (Better Auth)」节
References
- STANDARDS.md — 详细编码标准 + 完整代码实现
- PATTERNS.md — 正确模式 vs 反模式对照
- REVIEW-CHECKLIST.md — PR 审查清单