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
code
MUST: 用清晰的函数名/变量名/类型名替代注释 MUST: 复杂算法或非直觉的业务规则才需要注释 MUST: 注释解释 WHY,不解释 WHAT (代码本身说明 what) NEVER: 函数签名上方加 JSDoc 复述参数名和类型 — TypeScript 已经表达了 NEVER: 注释掉的代码 — 直接删除,git 有历史 NEVER: 分隔线注释 (// ────────) — 用空行和函数拆分表达结构 NEVER: "显而易见"的注释 (// 设置 loading 为 true → setIsLoading(true))
Architecture: Page-Autonomous Structure
code
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 │ ├── contexts/ # Context 定义 └── utils/ # 全局工具函数
关键原则
code
MUST: 页面私有代码放 pages/{page}/ 下,不放 common/
MUST: 跨 2 个以上页面复用的代码提升到 common/
MUST: 数据获取逻辑放 services/ (按页面分文件),不内联在组件中
MUST: common/hooks/index.ts barrel re-export 所有公共 hooks
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,零业务 |
规则
code
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 组合复杂布局
NEVER: 同文件定义多个组件 — 拆分为独立文件
NEVER: 超过 150 行的组件 — 拆分
NEVER: Props 透传超过 2 层 — 用 Context
Slot 组合示例
typescript
interface Props<T> {
data: T;
leftArea?: ReactNode;
title?: ReactNode;
subtitle?: ReactNode;
operationArea?: ReactNode;
onPress?: (data: T) => void;
}
export default function CardContainer<T>({ data, leftArea, title, subtitle, operationArea, onPress }: Props<T>): ReactNode {
return (
<XStack onPress={() => onPress?.(data)}>
{leftArea ? <YStack flexShrink={0}>{leftArea}</YStack> : null}
<YStack flex={1}>
{title ? title : null}
{subtitle ? subtitle : null}
{operationArea ? operationArea : null}
</YStack>
</XStack>
);
}
Hook Taxonomy
前置规则: 禁止空壳 re-export
code
NEVER: 创建只做 re-export 的 hook 文件 (如 export { useX } from 'lib')
→ 直接从源库导入,空壳文件是死代码的温床
ONLY: 当你封装了自定义逻辑时,才值得创建 hook 文件
Hooks 严格分三类,每类有明确的约束:
① 数据 Hook (Data Hook)
职责:获取数据 + 管理 loading/error 状态。返回结构化对象。
code
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 模式
typescript
// 简单模式: 加载 + 重试
const [fetchKey, setFetchKey] = useState<number>(0);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
setErrorType(undefined);
async function load(): Promise<void> {
try {
const response = await fetchXxxPage();
if (cancelled) return;
const mappedError = getPageErrorType(response);
if (mappedError) {
setErrorType(mappedError);
setData(null);
} else {
setData(response.data ?? null);
}
} catch {
if (cancelled) return;
setErrorType(PageErrorType.NETWORK);
setData(null);
} finally {
if (!cancelled) setIsLoading(false);
}
}
void load();
return () => {
cancelled = true;
};
}, [fetchKey]); // + 其他依赖如 session
const handleRetry = (): void => {
setFetchKey((k) => k + 1);
};
② 派生 Hook (Derived Hook)
职责:纯计算/数据转换,零副作用。只用 useMemo。
code
MUST: 只使用 useMemo,不使用 useEffect / useState MUST: 纯函数语义: 相同输入永远相同输出 MUST: 命名体现数据来源: useDataFromQuery, useFormattedPrice
③ 副作用 Hook (Effect Hook)
职责:管理单一副作用 (事件监听、定时器、性能埋点)。
code
MUST: 只有 1 个 useEffect MUST: useRef 保存最新回调 (防止闭包过期) MUST: cleanup 函数清理所有副作用 MUST: 返回类型为 void (不返回状态)
组合
typescript
// pages/chat/hooks/useChatPageData.ts — 数据 hook (单一 effect) // pages/chat/hooks/useChatPolling.ts — 副作用 hook (单一 effect) // 在 Screen 层组合: const pageData = useChatPageData(chatId); useChatPolling(pageData.silentRefresh, pollingInterval);
详见 STANDARDS.md 和 PATTERNS.md
Type Safety
code
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: ! 非空断言
判别联合 Context 示例
typescript
interface ChatDirectContext {
scene: 'direct';
peerId: string;
getLogParams: () => DirectLogParams;
}
interface ChatGroupContext {
scene: 'group';
groupId: string;
memberCount: number;
getLogParams: () => GroupLogParams;
}
type ChatPageContext = ChatDirectContext | ChatGroupContext;
Error Handling
code
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
标准错误流
code
API 响应 → getPageErrorType(resp) → PageErrorType | undefined
↓
undefined = 成功,继续处理
PageErrorType = 设置错误状态
↓
Screen: <PageStatusView errorType={errorType} onRetry={handleRetry} />
Style System: Tamagui-First
code
MUST: 布局用 Tamagui props (bg, px, py, gap, flex, borderRadius) MUST: 颜色只用 theme tokens ($color, $blue9, $gray4, $background) MUST: 间距只用 size tokens ($1, $2, $3, $4) NEVER: className / Tailwind NEVER: 硬编码颜色 (#ffffff, rgba(...)) AVOID: inline style — 仅 Tamagui 不支持的属性 (需注释原因)
Naming Conventions
文件
code
组件: 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/ (page-status-view/, operation-button/) Barrel: 仅 common/hooks/index.ts 需要 barrel re-export,页面级 hooks 直接 import 具体文件
代码
code
组件名: 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
typescript
// 1. React / React Native 核心
import { useState, useCallback } from 'react';
import { Alert } from 'react-native';
// 2. 第三方框架
import { useRouter } from 'expo-router';
import { YStack, Text } from 'tamagui';
// 3. 项目 common/ (shared)
import type { ProfilePageData } from '@/common/typings/profile';
import { useAuth } from '@/common/hooks';
import PageLoading from '@/common/components/page-loading';
import { getPageErrorType } from '@/common/components/page-status-view';
// 4. 项目 services/ (数据获取)
import { fetchProfilePage } from '@/services/profile';
// 5. 页面相对路径 (同页面内)
import { POLLING_INTERVAL } from '../consts';
import type { ChatLogParams } from '../types';
import ChatBubble from './chat-bubble';
code
MUST: type-only imports 使用 import type { X }
MUST: 组间保留空行
MUST: 同组内按字母排序
MUST: common/hooks 用 barrel import: from '@/common/hooks'
Enforcement
写代码时自动检查:
- •页面私有代码是否放在 pages/{page}/ 下?
- •组件是否超过 150 行?→ 拆分,依据具体情况拆分,如果真的要这么多,那就不拆了
- •Hook 是否属于明确的分类 (数据/派生/副作用)?
- •Hook 是否有多个 useEffect?→ 拆分
- •返回类型是否显式标注?→ 补充 ReactNode / void
- •是否有 any/as/! ?→ type guard
- •error 是否用 enum?→ 不用 string
- •不等待的 Promise 是否加了 void?
- •样式是否用 Tamagui token?→ 不用 className/硬编码
- •import 是否按规范分组?
References
- •STANDARDS.md — 详细编码标准 + 完整代码实现
- •PATTERNS.md — 正确模式 vs 反模式对照
- •REVIEW-CHECKLIST.md — PR 审查清单