资讯动态

Onyx 移动端(React Native + Expo)开发标准实战指南:从设计令牌到数据层与测试规范

发布时间:2026/9/10 2:49:03 来源:尧图企业网站定制
Onyx 移动端React Native Expo开发标准实战指南从设计令牌到数据层与测试规范【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer导读本文基于 Onyxdanswer仓库中mobile/CLAUDE.md这份移动端开发标准文档系统讲解在mobile/React Native Expo 应用中构建 UI、使用间距与设计令牌、组织 HTTP 与数据层、导航、编写测试以及消费共享包onyx-ai/shared的全套工程规范。读完本文你将掌握移动端与 Web 端在样式体系上的本质差异尤其是类名数字即像素这一最大陷阱、跨端设计令牌的落地链路以及一套可直接套用的组件复用、数据缓存与单元测试实践。一、文档定位mobile/ 的独立标准与 Web 的边界mobile/CLAUDE.md是mobile/目录Onyx 的 React Native Expo 应用中 AI Agent 与开发者的权威开发标准。它补充但不继承web/AGENTS.md的规则移动端没有 DOM使用 NativeWind而非 Web 的 Tailwind、expo-router 和 RN 原生组件因此 Web 侧关于 HTML/CSS、useSWR、Opal 组件等规则在这里均不适用。唯一跨端共享的是设计令牌词汇表通过onyx-ai/shared包提供。这一点从 mobile/tailwind.config.js 可以印证它通过require(onyx-ai/shared/nativewind-theme)和require(onyx-ai/shared/nativewind-typography)引入跨端主题扩展而不是像 Web 那样直接使用 Tailwind 默认主题。适用前提本文所有路径、命令与配置均以当前仓库实际内容为准运行环境为 Node bun移动端脚本在 mobile/package.json 中定义。二、构建 UI 的第一原则复用优先reuse before you build在手工编写任何组件或页面之前先检查是否已有匹配的组件按以下顺序排查移动端已有直接复用优先扫描mobile/src/components/ui/*基础 UI 原语、外壳布局mobile/src/components/{settings,sidebar,auth,chat}、mobile/src/icons/*以及其他mobile/src/components/*目录。实际仓库中ui/下已有button.tsx、text.tsx、text-input.tsx、icon.tsx、card.tsx、sheet.tsx、tabs.tsx、switch.tsx、spinner.tsx、popover.tsx、separator.tsx、content.tsx、line-item-button.tsx、select-button.tsx等现成原语见 mobile/src/components/ui。只有 Web 有WebOpal 的web/lib/opal/src/或web/src/refresh-components/是设计的事实来源source of truth。不要手工写一个分叉的相似品——先停下来确认要么通过port-web-component-to-mobileskill 做像素/行为级精确移植要么用现有原语组合实现。移植时应尽量在布局、间距、颜色、交互上与 Web 对应组件保持一致平台无法做到完全一致的必须记录有意的偏差document any deliberate divergence。三、间距系统类名数字即像素最大的移植陷阱这是从 Web 移植到移动端时最大的一个坑。3.1 两种完全不同的命名语义移动端间距类名解析为与类名数字相等的像素值——px-24 24pxgap-8 8pxh-12 12px。Web 端使用Tailwind 默认步进标度p-6 第 6 步 1.5rem24px。物理尺寸相同但命名完全不同物理尺寸WebTailwind 步进移动端px 命名令牌8pxp-2p-816pxp-4p-1624pxp-6p-243.2 底层原理设计令牌的生成链路移动端这个数字即像素的标度并非魔法而是由共享包的设计令牌构建链生成的。整个流水线位于 web/lib/shared/style-dictionary.config.mjsStyle Dictionary v4 编程式 API其 TOKEN MODEL 为间距令牌在 web/lib/shared/tokens/size.json 中以rem定义例如spacing-block-24: 1.5rem、spacing-block-16: 1remtoPx辅助函数执行parseFloat(v) * 16rem × 16 转为 pxjs/nativewind-themeformat 把spacing-block-*/spacing-inline-*解析为纯 px 数字例如spacing[24] 24同时把radius-*解析为 px 圆角、颜色令牌映射为var(--name)生成dist/nativewind-theme.cjs被 mobile/tailwind.config.js 作为theme.extend消费。之所以要烘焙成 px是因为React Native 无法使用rem或var()作为尺寸单位所以尺寸必须在构建期解析为具体像素值。3.3 必须遵守的规则绝不把 Web 的间距类名数字照搬到移动端。换算规则Web Tailwind 步进N→ 移动端N × 4px或者直接使用你实际想要的 px 值——在移动端数字本身就是 px。只用标度上真实存在的令牌键0,2,4,6,8,10,12,16,20,24,28,32,36,40,44,48,…size.json 中spacing-block-*覆盖到 160。不存在的键如p-3会回退到 Tailwind 默认 rem 标度——务必避免。把反复出现的间距集中到布局原语中不要在每个页面重复硬编码。屏幕内边距gutter由外壳组件持有mobile/src/components/auth/AuthScreenShell.tsx、mobile/src/components/chat/ChatScreen.tsx其中的CenteredContent持有居中屏幕 gutter。新建页面/空状态应组合现有外壳而不是硬写px-24。这个复用外壳的设计在 mobile/src/components/auth 与 mobile/src/components/chat 目录中可见一斑认证、聊天等场景都沉淀为独立的 shell/layout 组件供页面组合。四、文本、输入框、图标与颜色规范4.1 文本一律走/components/ui/text所有文本必须通过mobile/src/components/ui/text.tsx的Text组件渲染其 props 为font/color字符串枚举TextFont/TextColor类型源自onyx-ai/shared/contracts。绝不直接使用 React Native 的Text包括测试代码中。从实现看mobile/src/components/ui/text.tsxText内部把枚举映射为字面量类名font-heading-h1、text-text-04等这样 NativeWind 的类扫描器才能拾取同时提供nowrap单行裁剪不显示省略号与maxLines最多 N 行、尾部省略两个便捷 prop内部转换为numberOfLinesellipsizeMode。注意react-native的TextInput与Text无关可以放心使用但字段输入优先用mobile/src/components/ui/text-input.tsx。4.2 图标默认导出 Icon组件图标以默认导出形式存在于mobile/src/icons/*通过mobile/src/components/ui/icon.tsx的Icon组件渲染Icon as{SvgFoo} size{…} classNametext-text-… /4.3 颜色只用 Onyx 语义类无dark:修饰符使用 Onyx 语义类bg-background-*、text-text-*、border-border-*它们在运行时通过 mobile/src/app/_layout.tsx 中的vars()provider 解析light/dark 取自onyx-ai/shared/nativeconst lightTheme vars(varsLight); const darkTheme vars(varsDark); const themeVars colorScheme dark ? darkTheme : lightTheme; // GestureHandlerRootView style{themeVars} classNameflex-1禁止dark:修饰符禁止裸写 Tailwind 颜色。背后的原理同样是 RN 无法像 Web CSS 那样运行时翻转变量web/lib/shared/style-dictionary.config.mjs的js/native-varsformat 直接生成两份已解析的具体十六进制值映射varsLight/varsDark如varsLight[--text-05] #000000e5、varsDark[--text-05] #fffffff2由根布局按系统配色方案切换——这与 Web 用 CSS:root/.dark变量的模型是对偶的。五、HTTP 与数据层5.1 HTTP 客户端apiFetchT所有请求走mobile/src/api/client.ts的apiFetchT自动注入 bearer token、把错误归一化为ApiError。关键点是getBaseUrl()已经自动追加了/api前缀所以调用时路径是裸的apiFetch(/chat/...); apiFetch(/me);从 mobile/src/api/config.ts 可以看到前缀处理逻辑API_PREFIX默认/api适配 nginx 前置部署代理会剥离它裸后端开发时可设空串且base URL 是惰性解析的——优先取会话中存储的服务器地址getStoredServerUrl()没有时才回退到EXPO_PUBLIC_API_URL仅开发用且EXPO_PUBLIC_*会打进客户端包只能放 base URL绝不能放密钥。每次请求惰性解析意味着切换实例后下一次调用立即生效配置错误也会表现为可捕获的 rejected query 而非模块加载崩溃。apiFetch的其他实现细节见 mobile/src/api/client.ts普通对象 body 自动 JSON 序列化并补Content-Type字符串 /FormData/URLSearchParams/Blob/ArrayBuffer/ 类型化数组原样透传防止二进制上传被误 JSON 化auth?: boolean | stored控制鉴权行为stored跳过刷新等待供刷新 token 自身调用使用非 2xx 统一转ApiError解析 FastAPI 的detail字符串或校验错误数组{loc, msg, type}对非 JSON 的 2xx做了防护避免原始SyntaxError逃逸并被错误重试。唯一的例外是流式聊天调用它使用expo/fetch以获得可读的响应体详见 docs/mobile-chat。5.2 服务端状态TanStack Query serverUrl 键服务端状态使用 TanStack Query且查询键以serverUrl为键见 mobile/src/api/query-keys.tsme: (serverUrl) [me, serverUrl]、chatSession: (serverUrl, sessionId) [chat-session, serverUrl, sessionId]等这样切换实例后绝不会串用上一个后端的数据。缓存默认配置mobile/src/query/client.tsstaleTime: 30_000、gcTime与持久化窗口一致24h、认证错误不重试、refetchOnWindowFocus必须为 true否则query/focus.ts的 AppState→focusManager 桥失效。5.3 隐私与持久化未加密 MMKV 的排除名单缓存持久化到未加密的 MMKV通过tanstack/query-sync-storage-persistermakeMmkvStorage因此任何 PII 键聊天内容、身份信息都必须通过NON_PERSISTED_KEY_PREFIXES排除该名单定义在mobile/src/query/client.ts已排除me、agents、workspaceSettings、userProjects、userProject、userRecentFiles等前缀另有一条默认拒绝规则键首段以chat-开头的查询一律不持久化未来新增聊天类键无需再手动登记选择器类键connector 类型、per-agent 工具 id有意保留持久化它们本身不含可读 PII持久化能让启动后立刻发送时仍尊重用户已保存的来源/工具选择。同时切换账号由sessionManager的purgeCache负责登录/登出都会同时清空内存与磁盘缓存与持久化排除名单互为补充。六、导航expo-router 与认证门导航使用expo-router。规则要点路由组是路径透明的app/(app)/index.tsx/认证路由是命令式的位于mobile/src/components/auth/AuthGate.tsx纯逻辑抽在authRoute.ts。实现上不使用Redirect根布局里useFocusEffect没有可绑定的聚焦路由而是router.replace() 渲染覆盖层错误时AuthUnreachable可重试、加载时AuthSplash导航表面是可折叠的侧边栏浮层基于 Portalmobile/src/components/sidebar不是 tab bar布局中用useGlobalSearchParams页面中用useLocalSearchParams。从根布局 mobile/src/app/_layout.tsx 可见其组合结构GestureHandlerRootView携带vars()主题→KeyboardProvider→SafeAreaProvider→PersistQueryClientProvider→SidebarProvider→AuthGate→Stack且PortalHost是主题根节点的最后一个子节点保证侧边栏浮层渲染在所有页面之上并继承vars()主题与安全区 insets。七、测试规范7.1 运行器与门禁运行器jest-expo。测试放在__tests__/匹配src/**/__tests__/**/*.test.ts?(x)门禁命令bun run typecheck、bun run lint、bunx jest脚本定义见 mobile/package.json。7.2 jest 全局变量必须显式导入从jest/globals导入describe/it/expect/jest/beforeEach——因为 TS 配置没有携带 ambient 测试类型。所有 import 放在文件顶部之后再写jest.mock(...)babel 会提升 mock这也满足import/first规则。7.3 原生 mock 集中管理MMKV 自 mock、expo-secure-store手动 mock 位于__mocks__/、重置逻辑在jest.setup.ts全部集中化测试文件无需各自造轮子。7.4jest.mocked()对泛型函数的坑对于apiFetchT这类泛型函数jest.mocked()会推断出never需要显式 castapiFetch as unknown as Mock (p: string, i?: ApiFetchInit) Promiseunknown ; // Mock 来自 jest-mock7.5 不要在单元测试中 import reanimated 的 barrel 导出/components/sidebar→Sidebar.tsx→ reanimated在 jest 下会崩溃Worklets not initialized。应直接 import 叶子组件如/components/sidebar/SidebarTab来保持组件可单测。这与 mobile/src/components/sidebar/index.ts 的 barrel 导出结构直接相关——barrel 会连带拉起重依赖叶子导入则不会。八、共享包onyx-ai/shared的协作规则onyx-ai/shared位于 web/lib/shared承载跨平台设计令牌中立契约/类型/工具onyx-ai/shared/native是RN 专属NativeWind 主题 / vars / 排版跨平台类型放/contracts绝不放进/native例如TextFont这个唯一的规范联合类型定义在web/lib/shared/src/contracts/typography.tsWeb 与移动端共用采用extract-on-proven-reuse先验证被复用了再抽取策略而不是提前抽象。移动端chat 层是有意原生写在mobile/src/chat/的不共享决策背景见 docs/mobile-chat/05-pr-roadmap.mdPR 2 Decision修改共享包后必须重建其dist在web/lib/shared下执行bun run build移动端以file:依赖消费distWeb jest 则通过moduleNameMapper从src解析。8.1 preinstall 构建链为什么必须是 preinstall移动端 package.json 中有一个关键脚本preinstall: cd ../web/lib/shared bun install bun run build它在 bun 链接file:依赖之前构建web/lib/shared的dist一个被 git 忽略的构建产物从而保证全新环境执行bun install时不会因onyx-ai/shared/dist不存在而失败。必须是preinstall而不是postinstall原因在于 bun 的链接农场link farm机制file:依赖被链接进node_modules时只有dist在链接时刻已存在才会被包含。日常迭代中对令牌/源码的活跃修改仍可通过在web/lib/shared下执行bun run dev热重建。九、总结移动端开发的四把尺子样式间距数字即像素Web 步进 ×4颜色只用语义类、由vars()运行时切换深浅色杜绝dark:与裸颜色组件先扫components/ui/*与外壳布局再动手Web 是设计源分歧必须记录数据apiFetch统一请求、查询键挂serverUrl防串库、PII 键禁止落盘到未加密 MMKV协作测试走 jest-expo 叶子导入共享包改动需重建distpreinstall保证安装链路完整。遵循这些标准可以在保持与 Web 端视觉一致的前提下让 Onyx 移动端拥有独立、健壮且可测试的工程形态。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价