expo-web-to-native

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web to Native

Web 转原生应用

A web React app does not convert to native — there is no transpiler. It migrates, screen by screen, the way a strangler fig grows around a tree and slowly replaces it: stand up a native shell, run the whole web UI inside it on day one, then strangle each screen into native in priority order. This skill is the spine that orders the work; each step hands off to an existing Expo skill rather than re-explaining it. It operationalizes Expo's From Web to Native with React — read that for the why.
mermaid
flowchart TD
    A1[1 · Assess: write the worklist] --> A2[2 · Scaffold Expo shell]
    A2 --> A3[3 · DOM-component shell<br/>· expo-dom · SHIP DAY ONE]
    A3 --> A4[4 · Strangle screens to native<br/>highest-value first · expo-router]
    A4 -->|more screens| A4
    A4 --> A5[5 · Wire data / auth / storage<br/>· expo-data-fetching]
    A5 --> A6[6 · Ship · eas-app-stores]
Web React应用并非转换为原生应用——不存在这样的转译器。它是逐步迁移的,逐个屏幕进行,就像绞杀榕缠绕树木并慢慢取代它的过程:先搭建一个原生外壳,第一天就在其中运行完整的Web UI,然后按照优先级逐个将屏幕重构为原生实现。本技能是迁移工作的核心框架;每个步骤都会衔接至现有的Expo技能,而非重复讲解细节。它将Expo的《使用React从Web转向原生》指南落地为可执行的步骤——若想了解迁移的原因,请阅读该文章。
mermaid
flowchart TD
    A1[1 · Assess: write the worklist] --> A2[2 · Scaffold Expo shell]
    A2 --> A3[3 · DOM-component shell<br/>· expo-dom · SHIP DAY ONE]
    A3 --> A4[4 · Strangle screens to native<br/>highest-value first · expo-router]
    A4 -->|more screens| A4
    A4 --> A5[5 · Wire data / auth / storage<br/>· expo-data-fetching]
    A5 --> A6[6 · Ship · eas-app-stores]

Principles

迁移原则

  • Migrate, don't rewrite. Never big-bang it; every step keeps the app shippable.
  • Ship on day one. The web UI runs in a DOM-component shell (step 3) before anything is nativized — that's the milestone; everything after is polish.
  • Strangle by value. Nativize the hot screens; leave the rest in the webview. Each DOM screen carries a ~2 MB web runtime — reason enough not to ship everything as DOM.
  • Nativize means redesign, not reskin. A strangled screen should look like Apple/Google shipped it, not the web page reskinned. Reach for
    @expo/ui
    first
    - it renders real SwiftUI/Compose, so it feels exactly like the OS; styled RN primitives are the fallback for custom layouts only. Plus platform navigation (
    expo-router
    : NativeTabs, large titles), liquid glass and native components via
    @expo/ui
    , and mobile UX (sheets, swipe, haptics). The web→native pattern map is
    ./references/native-patterns.md
    . If it still feels like a website, you ported instead of redesigned.
  • Verify by running, not compiling. A clean build proves nothing (a blank webview compiles fine). Run each screen — but judge content and behavior against the web original, not pixels (a nativized screen should look more native, not identical).
  • Orchestrate, don't reinvent. Each step routes into an existing skill. The value here is the order and the gotchas — the idiom-by-idiom mappings live in
    ./references/false-friends.md
    .
  • 迁移而非重写:绝不采用大爆炸式重构;每一步都要保证应用可发布。
  • 首日即可发布:在完成任何原生重构前,Web UI就可以在DOM组件外壳中运行(步骤3)——这是关键里程碑;后续所有工作都是优化。
  • 按价值优先级重构:优先对高频使用的屏幕进行原生重构;其余屏幕保留在Web视图中。每个DOM屏幕会占用约2MB的Web运行时——这足以成为不将所有内容都以DOM形式发布的理由。
  • 原生重构意味着重新设计,而非换皮:重构后的屏幕应具备苹果/谷歌官方应用的质感,而非Web页面的换皮版本。优先使用
    @expo/ui
    ——它会渲染真正的SwiftUI/Compose组件,因此完全贴合系统风格;只有在需要自定义布局时,才退而求其次使用RN基础组件。此外还需适配平台导航(
    expo-router
    :NativeTabs、大标题)、液态玻璃效果及通过
    @expo/ui
    实现的原生组件,以及移动端UX(底部弹窗、滑动操作、触觉反馈)。Web→原生的模式映射可查看
    ./references/native-patterns.md
    。如果最终效果仍像网站,说明你只是移植而非重新设计。
  • 通过运行验证,而非编译:成功编译不代表任何问题(空白Web视图也能正常编译)。要运行每个屏幕进行验证——但需对照Web原版验证内容和行为,而非像素(原生重构后的屏幕应更具原生质感,而非与Web版完全一致)。
  • 统筹协调,而非重复造轮子:每个步骤都会衔接至现有技能。本技能的价值在于提供步骤顺序注意事项——Web与原生惯用法的映射可查看
    ./references/false-friends.md

Run it as a loop (recommended)

推荐以循环方式执行

The migration is a long repeat-until-done loop, so the first move is to write the goal objective and launch it — not to grind screens by hand. Fill the objective in
./references/run-as-goal.md
for this app and present it; it re-reads this skill every iteration, so each
/goal
turn reloads the playbook + worklist and drives the next screen (it even self-bootstraps the assess step). Then run
/goal
with it — or, if the harness can't loop, write it to
migration-goal.md
and have the user launch it. The steps below are what each iteration does; run them by hand only if you're not looping.
迁移是一个需要重复执行直至完成的长期过程,因此第一步是撰写目标并启动——而非手动逐个处理屏幕。为本应用填写
./references/run-as-goal.md
中的目标并启动;它会在每次迭代时重新读取本技能,因此每次执行
/goal
都会重新加载指南和任务清单,并推进下一个屏幕的迁移(甚至可以自动完成评估步骤的初始化)。之后使用该目标执行
/goal
;如果无法循环执行,可将目标写入
migration-goal.md
并让用户启动。以下是每次迭代的具体步骤;仅当无法循环时才手动执行。

The migration

迁移步骤

No repo to migrate - just building native fresh as a web dev? You don't need these steps: use
expo-router
, and keep
./references/false-friends.md
open for the web→native idiom map. Everything below assumes an existing web app.
无现有仓库可迁移 - 作为Web开发者从零开始构建原生应用?你无需遵循以下步骤:直接使用
expo-router
,并打开
./references/false-friends.md
查看Web→原生惯用法映射。以下所有步骤均假设存在现有Web应用。

1. Assess → write the worklist

1. 评估 → 撰写任务清单

Read the repo and produce
migration-progress.md
, the durable worklist the rest of the migration checks off. Make two cuts:
  • Screens vs backend. Page routes (
    page.tsx
    ) are screens you migrate; server routes (
    route.ts
    ), the ORM, and auth handlers stay server-side. Decide the backend once: keep it deployed (the native app becomes an HTTP client) or move it to EAS Hosting (
    eas-hosting
    ).
  • Bucket each screen by how it should land: port-as-is (presentational → ships in a DOM webview), nativize-now (hot, or needs native feel — gestures, lists, keyboard), nativize-later, or hybrid (a native shell around a web sub-tree, e.g. a chat list wrapping a markdown renderer).
Note the framework signals as you read — RSC vs client, Tailwind/shadcn, where data is fetched — since they decide how each screen ports (false-friends has the mappings; async Server Components in particular must be split into a client fetch + a presentational component before they can move). Flag third-party services/SDKs too — browser SDKs don't carry over (
false-friends
Services & SDKs); payments especially is a fork, not a swap (in-app digital goods must use store IAP via RevenueCat, ~30% — not Stripe), a business-model call to make now, not at App Store review. The worklist is only trustworthy once every route is sorted and every screen bucketed.
读取仓库内容并生成
migration-progress.md
,这是后续迁移工作的持久任务清单。需完成两项梳理:
  • 屏幕与后端分离:页面路由(
    page.tsx
    )是需要迁移的屏幕;服务器路由(
    route.ts
    )、ORM和认证处理程序保留在服务器端。只需确定一次后端方案:保持现有部署(原生应用作为HTTP客户端)或迁移至EAS Hosting(
    eas-hosting
    )。
  • 为每个屏幕分类:确定每个屏幕的处理方式:原样移植(展示型页面→以DOM Web视图形式发布)、立即原生重构(高频使用或需要原生体验——手势、列表、键盘交互)、后续原生重构,或混合模式(原生外壳包裹Web子树,例如聊天列表包裹markdown渲染器)。
读取时记录框架特征——RSC与客户端组件、Tailwind/shadcn、数据获取位置——这些将决定每个屏幕的移植方式(
false-friends
文档中有对应映射;尤其是异步Server Components,必须拆分为客户端获取组件+展示组件后才能迁移)。同时标记第三方服务/SDK——浏览器SDK无法直接迁移(
false-friends
Services & SDKs章节);支付功能尤其需要重新实现,而非替换(应用内数字商品必须使用应用商店的IAP,通过RevenueCat实现,抽成约30%——不能使用Stripe),这是需要提前确定的业务决策,而非等到应用商店审核时才处理。只有当所有路由都已梳理、所有屏幕都已分类后,任务清单才具备可信度。

2. Scaffold the shell

2. 搭建原生外壳

create-expo-app
, then mirror the web routes in Expo Router — Next's tree maps almost 1:1 (note
[id]/page.tsx
[id].tsx
, and routes may live in
src/app/
). Empty screens, one per route.
执行
create-expo-app
,然后在Expo Router中镜像Web应用的路由——Next.js的路由树几乎可以1:1映射(注意
[id]/page.tsx
[id].tsx
,路由可能位于
src/app/
目录)。为每个路由创建空屏幕。

3. Shell it in DOM components — the day-one milestone

3. 集成DOM组件外壳——首日里程碑

Bring every screen over as a DOM component (
'use dom'
, per the
expo-dom
skill) rendered by its native route, so the whole app runs on a phone before anything is nativized. Expect per-screen edits - unwrapping Server Components, swapping framework imports (
next/link
), carrying the styling over - all covered in false-friends. Then verify by running (below); this is shippable to TestFlight as-is.
将所有屏幕以DOM组件形式(遵循
expo-dom
技能中的
'use dom'
规范)接入对应的原生路由,这样在完成任何原生重构前,整个应用就能在手机上运行。预计需要对每个屏幕进行编辑——拆分Server Components、替换框架导入(如
next/link
)、迁移样式——这些都在
false-friends
文档中有说明。然后通过运行验证(见下文);此时应用已可发布至TestFlight。

4. Strangle screens to native — by value

4. 按价值优先级重构屏幕为原生实现

Walk
migration-progress.md
top-down. For each screen, redesign it native - don't port the web layout. Reach for
@expo/ui
first
(real SwiftUI/Compose - buttons, lists, sheets, pickers, sliders;
./references/native-patterns.md
maps which web pattern becomes which native component), then platform navigation (
expo-router
- NativeTabs, large titles) and mobile UX (swipe, haptics, momentum/inverted scroll); RN primitives only for custom layouts. Consult
./references/false-friends.md
for each idiom.
@expo/ui
and DOM components both run in Expo Go (SDK 56+) - a dev build (the
expo-dev-client
skill) is only needed for custom native modules. Verify content and behavior against the running web original (the look should become more native), then check it off. One screen per pass, app shippable throughout. It's a loop over a durable worklist, so it can run unattended - hand it to a goal loop (
./references/run-as-goal.md
).
按照
migration-progress.md
的优先级顺序处理。对于每个屏幕,重新设计原生版本,而非移植Web布局。优先使用
@expo/ui
(提供真正的SwiftUI/Compose组件——按钮、列表、底部弹窗、选择器、滑块;
./references/native-patterns.md
映射了Web模式对应的原生组件),然后适配平台导航(
expo-router
——NativeTabs、大标题)和移动端UX(滑动操作、触觉反馈、惯性/反向滚动);仅在需要自定义布局时使用RN基础组件。每个惯用法的处理可参考
./references/false-friends.md
@expo/ui
和DOM组件均可在Expo Go(SDK 56+)中运行——只有在使用自定义原生模块时才需要开发构建版本(
expo-dev-client
技能)。对照运行中的Web原版验证内容和行为(外观应更具原生质感),然后标记为完成。每次处理一个屏幕,全程保证应用可发布。这是一个基于持久任务清单的循环过程,可无人值守执行——将其交给目标循环(
./references/run-as-goal.md
)即可。

5. Wire data, auth, and storage

5. 对接数据、认证与存储

The web data layer doesn't survive the move - relative fetches, cookie sessions,
localStorage
, and env vars all change (swaps in false-friends). Use
expo-data-fetching
for requests and caching; add
eas-hosting
if the backend moved to EAS Hosting.
Web应用的数据层无法直接迁移——相对路径请求、Cookie会话、
localStorage
和环境变量均需调整(
false-friends
文档中有对应的替换方案)。使用
expo-data-fetching
处理请求和缓存;如果后端已迁移至EAS Hosting,则添加
eas-hosting
配置。

6. Ship

6. 发布

eas-app-stores
for the store builds (App Store / Play / TestFlight), EAS Update for OTA pushes after.
使用
eas-app-stores
构建应用商店版本(App Store / Play Store / TestFlight),发布后使用EAS Update进行OTA推送更新。

Verify by running, not compiling

通过运行验证,而非编译

A green
expo export
proves a screen bundles, not that it renders — a screen can build and still render blank or mis-render. So after the shell and after every nativized screen, compare the two running apps for the same route:
  • Web original — capture it with
    agent-browser
    (vercel-labs CLI):
    open
    the route,
    snapshot --json
    the accessibility tree,
    screenshot
    .
  • Native — drive the simulator with
    argent
    :
    describe
    /
    debugger-component-tree
    for structure,
    flow
    to replay the check each pass.
Pass on parity of content and behavior — not pixels: a nativized screen should look more native than the web, never identical (the DOM-shell stage is the exception — there it is the web UI, so it should match). Feel is part of native and can't be screenshotted — for screens with transitions or gestures, capture a short recording, not just a still (see
native-patterns.md
→ Feel). This loop is opinionated about its tooling: if
agent-browser
or
argent
isn't installed, ask the user and install it before proceeding — don't fall back to manual screenshots. Full recipe and setup in
./references/verify-on-device.md
.
expo export
成功只能说明屏幕可打包,而非可正常渲染——屏幕可能编译成功但渲染空白或显示异常。因此在搭建外壳后、每次完成原生重构屏幕后,需对照同一路由的两个运行中应用进行验证:
  • Web原版——使用**
    agent-browser
    **(vercel-labs CLI)捕获:
    open
    打开路由,
    snapshot --json
    获取无障碍树,
    screenshot
    截图。
  • 原生版本——使用**
    argent
    **驱动模拟器:
    describe
    /
    debugger-component-tree
    查看结构,
    flow
    在每次迭代时重复验证流程。
验证通过的标准是内容和行为一致——而非像素完全相同:原生重构后的屏幕应比Web版更具原生质感,而非完全一致(DOM外壳阶段除外——此时就是Web UI,因此应与Web版完全匹配)。原生体验的触感无法通过截图捕捉——对于带有过渡动画或手势的屏幕,需录制短视频而非仅截图(见
native-patterns.md
→Feel章节)。本流程对工具选型有明确要求:如果未安装
agent-browser
argent
,请告知用户并先安装——不要退而求其次使用手动截图。完整的配置和流程可查看
./references/verify-on-device.md

References

参考资料

  • ./references/false-friends.md
    — web idiom → native equivalent + the gotcha for each. The lookup for steps 3–5, and for any web dev unlearning idioms.
  • ./references/native-patterns.md
    — web UX pattern → native redesign (
    @expo/ui
    -first). The step-4 redesign playbook so screens feel OS-native, not reskinned.
  • ./references/verify-on-device.md
    — the two-agent parity recipe: drive the web app (browser agent) and the native app (argent), open the same route, compare.
  • ./references/run-as-goal.md
    — a ready-shaped, migration-specific goal objective for driving step 4 unattended (re-reads this skill each iteration).
  • Expo — From Web to Native with React — the canonical guide this skill operationalizes.
  • ./references/false-friends.md
    ——Web惯用法→原生等效方案及注意事项。是步骤3-5的参考手册,也是Web开发者摒弃旧惯用法的指南。
  • ./references/native-patterns.md
    ——Web UX模式→原生重设计方案(优先使用
    @expo/ui
    )。是步骤4的重设计指南,确保屏幕具备系统原生质感而非换皮效果。
  • ./references/verify-on-device.md
    ——双代理一致性验证流程:驱动Web应用(浏览器代理)和原生应用(argent),打开同一路由并进行对比。
  • ./references/run-as-goal.md
    ——预定义的迁移专用目标,用于无人值守执行步骤4(每次迭代时重新读取本技能)。
  • Expo — 使用React从Web转向原生——本技能落地的官方指南。