ISSUE / 048
从 JSX 统一 UI,到 Agent First:多端应用最终会变成什么
最近看到一个很有意思的观点。 随着 Expo UI 的发展,同一棵 JSX 组件树,未来可能分别落到 iOS 的 SwiftUI、Android 的 Jetpack Compose,以及 Web 的 React DOM 上。 开发者依旧使用 React 和 JSX 描述界面,最终负责渲染的却是不同平台自…

最近看到一个很有意思的观点。
随着 Expo UI 的发展,同一棵 JSX 组件树,未来可能分别落到 iOS 的 SwiftUI、Android 的 Jetpack Compose,以及 Web 的 React DOM 上。
开发者依旧使用 React 和 JSX 描述界面,最终负责渲染的却是不同平台自己的 UI 系统。
这让我想到 Kotlin Multiplatform,也就是 KMP。
KMP 一直强调共享业务逻辑,同时保留 SwiftUI 和 Compose 两套平台 UI。Expo UI 展现出的方向,则更进一步:开发者使用 JSX 描述产品结构,由不同平台 Adapter 把这些描述映射到各自的 UI 后端。
如果继续沿着这个方向推演,JSX 可能逐渐成为一种跨平台 UI 描述语言。
但当我把这个想法放到自己的项目中时,我发现真正值得思考的问题,还要再往上一层。
我负责的产品需要覆盖 Desktop Web、H5、iOS 和 Android。四个平台的产品语义高度一致,主要区别集中在布局、控件、导航方式和少量系统交互。
那么,我们到底应该共享什么?
继续推演下去,这个问题最终又指向了一个更前卫的方向:
当 Agent 成为主要开发者之后,多端应用会演化成什么形态?
一、JSX 为什么开始让人想到 KMP
传统 React Native 提供了一套跨平台组件体系。
开发者写:
<View> <Text>Hello</Text></View>React Native 再通过自己的渲染架构,把这些组件映射到 iOS 和 Android。
而 Expo UI 展现出的思路更接近:
JSX ↓平台 Adapter ├── SwiftUI ├── Jetpack Compose └── React DOM这里的 JSX,逐渐不再只是 React DOM 或 React Native View 的组件树。
它开始像一种声明式的产品界面语言。
开发者描述:
<Button /><List /><Form />底层平台负责决定这个按钮究竟是 SwiftUI Button、Compose Button,还是浏览器中的 button。
这与 KMP 的理念存在一个很有意思的交集。
KMP 通常共享:
业务模型网络请求状态管理Use CaseViewModel平台分别实现:
SwiftUICompose而 JSX 这条路线,则可能把共享边界继续向 UI 语义推进:
共享产品结构与交互意图 ↓平台负责最终控件与系统体验这意味着,未来的跨平台开发可能会越来越关注“产品语义共享”,而减少对像素级组件复用的执着。
二、现实项目里,真的需要构建通用 JSX Adapter 吗
把这个方向落到现实项目,情况往往没有概念图这么干净。
我的项目有四个端:
Desktop WebH5iOSAndroid具体技术栈是:
Next.js├── Desktop Web└── H5
React Native├── iOS└── AndroidWeb 和 H5 是同一个 Next.js 项目,通过响应式布局和少量条件判断完成差异化。
iOS 和 Android 是同一个 React Native 项目。
从产品角度看,这是四端。
从运行时角度看,主要只有两个渲染后端:
React DOMReact Native这让问题简单了很多。
我没有必要从头构建一套能够覆盖所有产品、所有控件、所有平台的通用 UI Adapter。
更现实的方案,是围绕自己的产品手搓一层有限的适配体系:
Shared Product Core├── Next.js Adapter│ ├── Desktop Web│ └── H5└── React Native Adapter ├── iOS └── AndroidNext.js 和 React Native 都能直接消费 TypeScript package。
没有跨语言桥接,没有 Swift、Kotlin 和 TypeScript 之间的模型转换,也不需要引入 Rust、WASM 或 KMP。
我们已经天然处在一个非常适合共享产品逻辑的位置。
接下来真正需要回答的问题是:
产品中哪些内容应该进入 Shared Product Core?
三、最值得共享的,是产品语义
以菜单筛选为例。
Desktop Web 可能使用固定侧边栏。
H5 可能使用 Bottom Sheet。
iOS 和 Android 可能进入一个独立筛选页面。
从 UI 看,它们是三种交互。
从产品语义看,用户完成的是同一件事情:
打开筛选修改筛选条件应用筛选请求新的菜单数据保存筛选状态关闭筛选入口这些行为可以被建模成统一事件:
type MenuFilterEvent = | { type: 'filter_opened' } | { type: 'filter_changed'; value: MenuFilter } | { type: 'filter_applied' } | { type: 'filter_reset' } | { type: 'url_restored'; value: MenuFilter } | { type: 'session_restored'; value: MenuFilter };状态也可以保持平台无关:
type MenuFilterState = { draft: MenuFilter; committed: MenuFilter; status: 'idle' | 'loading' | 'error';};产品逻辑处理事件后,返回新的状态,以及需要执行的 Effect:
type MenuFilterEffect = | { type: 'fetch_menu'; filter: MenuFilter } | { type: 'sync_url'; filter: MenuFilter } | { type: 'persist_filter'; filter: MenuFilter } | { type: 'close_filter' };Shared Product Core 只负责表达:
用户确认了筛选条件需要加载新数据需要保存状态需要关闭当前筛选入口至于 close_filter 在具体平台上意味着什么,由 Adapter 自己解释。
在 Desktop Web 中,它可能什么都不做,因为筛选栏一直存在。
在 H5 中,它可能关闭 Bottom Sheet。
在 React Native 中,它可能调用 navigation.goBack()。
这样,产品流程只维护一份,平台交互依旧保留自己的实现。
四、isWeb 和 isMobile 应该停留在哪一层
响应式项目里,出现以下代码很正常:
return isMobile ? <MobileFilter /> : <DesktopFilter />;它表达的是组件结构差异,放在 UI 层通常没有问题。
风险出现在平台身份开始进入业务流程之后:
if (isMobile) { saveFilterToStore();}
if (isWeb) { syncFilterToUrl();}随着需求增加,代码里会不断出现:
isWebisMobileisNativeisIOSisAndroidisTabletisWechat最终,一项业务规则会依赖多个平台布尔值的组合。
更稳定的方式,是把平台身份翻译成产品策略。
例如:
type MenuPolicy = { filterPresentation: 'sidebar' | 'sheet' | 'screen'; syncFilterToUrl: boolean; persistFilterInSession: boolean; openCartAfterAddingItem: boolean;};Desktop Web:
const desktopWebPolicy: MenuPolicy = { filterPresentation: 'sidebar', syncFilterToUrl: true, persistFilterInSession: true, openCartAfterAddingItem: true,};H5:
const mobileWebPolicy: MenuPolicy = { filterPresentation: 'sheet', syncFilterToUrl: true, persistFilterInSession: true, openCartAfterAddingItem: false,};React Native:
const nativePolicy: MenuPolicy = { filterPresentation: 'screen', syncFilterToUrl: false, persistFilterInSession: true, openCartAfterAddingItem: false,};业务代码读取的是:
policy.persistFilterInSessionUI 代码读取的是:
policy.filterPresentation这样,产品核心不需要感知自己运行在浏览器、iPhone 还是 Android 手机上。
平台身份先经过 Adapter 和 Policy,最终变成一个明确的产品决策。
五、共享 Controller,比共享组件更实用
跨端架构很容易走向组件抽象。
例如:
CrossPlatformButtonCrossPlatformModalCrossPlatformListCrossPlatformTextField但控件层恰好是平台差异最密集的位置。
同一个“让用户选择筛选条件”的需求,可以对应:
Desktop Web 的 SidebarH5 的 Bottom SheetiOS 的 SheetAndroid 的 Screen如果强行把它们统一成一个组件,组件 API 很快会被大量平台参数占满。
更适合共享的单位,是 Headless Controller。
Controller 只关心:
当前是什么状态用户表达了什么意图下一步应该做什么平台需要执行什么 Effect例如“登录后继续加购”:
用户点击加购检查登录状态请求登录保存待执行意图登录成功恢复加购调用接口更新购物车展示结果UI 只发送:
controller.send({ type: 'add_to_cart_clicked',});Controller 返回:
{ state: { status: 'waiting_for_auth', }, effects: [ { type: 'request_authentication', }, ],}Next.js Adapter 可以把 request_authentication 映射成路由跳转。
React Native Adapter 可以把它映射成 Modal 或独立页面。
登录完成后,平台再把结果送回 Controller:
controller.send({ type: 'authentication_succeeded',});Controller 继续推进原来的产品流程。
这时,共享层管理的是产品行为,平台层管理的是呈现和系统能力。
六、到这里,我们还只是在讨论多端架构
如果项目只由人类开发者维护,到这里已经是一套相当实用的架构。
目录可能是:
apps/ next-app/ native-app/
packages/ product-core/ platform-contracts/ api-client/ analytics-schema/ next-adapter/ native-adapter/其中:
product-core 状态 事件 规则 Effect 错误模型 Selector
next-adapter URL Router Cookie sessionStorage SSR React DOM
native-adapter React Navigation AsyncStorage AppState Linking 系统权限这套设计可以解决大量重复实现问题。
但当我继续追问:
如果未来主要代码都由 Agent 完成,我只负责顶层设计,这套项目还会继续演化成什么?
问题发生了变化。
过去,我们设计共享核心,是为了让人类少写几遍代码。
进入 Agent First 之后,共享产品语义还有一个更关键的作用:
它会成为 Agent 理解、实现和验证产品的中间语言。
七、Agent 能快速写四份代码,但也能快速制造四份漂移
让 Agent 分别修改 Next.js、H5、iOS 和 Android,并不困难。
真正困难的是保证:
四端理解了同一个需求四端实现了同一种业务规则平台差异属于有意设计埋点和错误处理保持一致后续修改不会继续漂移如果需求只存在于 PRD 和聊天记录中,Agent 很容易在每个端做出不同解释。
所以 Agent First 的关键并不只是增加几个 Coding Agent。
项目需要一份更稳定、更结构化、更可执行的产品模型。
整体结构会逐渐变成:
Product Intent ↓Executable Product Model ↓Agent Implementation ↓Next.jsReact NativeTestsAnalyticsRelease Evidence人类继续向上移动。
Agent 承担越来越多实现工作。
八、产品语义会逐渐成为项目的“源代码”
以菜单筛选恢复为例,传统需求可能是一段自然语言:
用户从其他页面回到菜单页时,需要恢复当前浏览器 Session 中的筛选条件。URL 参数优先。
对于人类开发者,这句话通常足够开始工作。
对于需要自主完成多端实现的 Agent,这里面仍有很多空白:
什么时候读取 Session?
什么时候同步 URL?
恢复完成前能否请求菜单接口?
浏览器前进后退怎么处理?
H5 和 Desktop Web 是否一致?
React Native 是否也需要恢复?
什么情况下清除状态?
更适合 Agent 的需求表达,可能会逐渐变成:
feature: restore-menu-filter
goal: 用户返回菜单页时,恢复当前会话中的筛选条件
invariants: - URL 参数优先于 session 数据 - 关闭 tab 后 session 数据消失 - 恢复完成前不得请求菜单接口 - 只保存 filter 相关参数
platform_behavior: desktop_web: presentation: sidebar mobile_web: presentation: sheet ios: presentation: screen android: presentation: screen
out_of_scope: - 跨设备同步 - localStorage 持久化 - 服务端保存筛选条件
acceptance: - direct_url_overrides_session - navigation_return_restores_session - history_navigation_preserves_url - closed_tab_clears_filter这份描述同时包含:
产品目标业务不变量平台差异排除范围验收场景它可以驱动 Product Core 变更,也可以驱动 Next.js、React Native 和测试代码的实现。
九、项目会出现一层 Product IR
传统编译器会先把高级语言转换成中间表示,再生成面向不同平台的机器代码。
Agent First 的多端应用,也可能形成类似结构:
产品目标业务规则平台能力设计系统验收场景 ↓Product IR ↓Next.js ImplementationReact Native ImplementationTestsAnalyticsDocumentationRelease EvidenceProduct IR 可以包含:
type ProductIR = { entities: EntityDefinition[]; states: StateDefinition[]; events: EventDefinition[]; effects: EffectDefinition[]; policies: PolicyDefinition[]; scenarios: AcceptanceScenario[];};这里的 UI 也不需要抽象成一套真正可运行的跨平台组件。
它可以先抽象成语义节点:
type ViewSemanticNode = | { type: 'content_list'; source: 'menu_items'; } | { type: 'filter_entry'; presentation: 'platform_defined'; } | { type: 'primary_action'; intent: 'add_to_cart'; } | { type: 'feedback'; source: 'cart_result'; };Next.js Agent 读取这些语义,结合 Web 的设计系统和交互规范,生成 React DOM 实现。
React Native Agent 读取相同语义,结合 iOS 和 Android 的平台策略,生成 React Native 实现。
它们共享产品意图,保留平台呈现。
有趣的是,这又回到了文章开头的 JSX 与 KMP。
一开始,我们设想 JSX 是跨平台 UI 描述语言。
继续往 Agent First 推进后,真正稳定的中间表示可能比 JSX 更高一层。
JSX 依然属于实现。
Product IR 描述的是产品本身。
十、从共享 JSX,到共享产品世界
这条演化路径可以分成几个阶段。
第一阶段,共享 JSX 或跨平台组件。
一套组件树 ↓多个平台渲染第二阶段,共享业务逻辑和 Headless Controller。
状态事件规则Effect ↓Next.js UIReact Native UI第三阶段,共享结构化产品规格。
目标不变量平台策略验收场景 ↓Agent 实现第四阶段,形成 Product IR 和 Agent Work Graph。
Product Model ↓多个 Agent 并行工作 ↓多端实现与验证证据到了最后,团队维护的核心资产将逐渐变成:
领域语言产品模型平台能力清单架构边界验收场景证据流水线Web、H5、iOS 和 Android,则成为同一个产品世界在不同平台上的投影。
十一、Agent First 项目最重要的资产
很多团队谈 Agent First 时,关注的是 Prompt、MCP、Skill、多 Agent 编排和自动化脚本。
这些都能提升执行效率。
长期决定系统质量的资产,还包括下面几类。
稳定的领域语言
例如:
pending intentfilter precedencesession restorationauthentication continuationcart reconciliation每个概念都需要稳定的名称和明确的定义。
产品、设计、开发、测试和 Agent 使用同一种语言,需求才能在多端保持一致。
可执行的验收场景
例如:
Given URL 中存在筛选条件And session 中存在另一组筛选条件When 用户进入菜单页Then 使用 URL 中的筛选条件And 只发起一次菜单请求场景可以直接转成测试。
它也是 Agent 判断任务完成与否的依据。
明确的架构边界
例如:
Product Core 禁止依赖 React平台判断只能存在于 AdapterURL 只能由 Next.js Adapter 操作Native 平台分支只能存在于 Native AdapterDomain Event 禁止出现组件名称Agent 在既定边界内可以自主实现。
如果需要引入新的架构概念,就需要提交设计变更。
完整的证据链
每次任务完成后,Agent 需要交付:
修改了哪些产品行为影响了哪些平台各平台如何呈现哪些差异属于有意设计通过了哪些测试还有哪些风险人类最终审查的会逐渐接近一份 Release Bundle。
代码只是其中一项证据。
十二、人的角色会移动到哪里
当 Agent 承担大部分实现工作,人类的角色会越来越集中在:
定义问题设计产品语言确定架构边界限制决策空间批准新的产品概念审查行为证据承担最终责任你不需要逐行告诉 Agent 应该怎么实现。
你需要明确:
哪些行为必须统一哪些平台允许不同哪些规则拥有更高优先级Agent 可以在哪些范围内自主决策什么证据足以证明需求完成这是一种更高层级的工程工作。
过去,架构设计主要用于协调人类开发者。
Agent First 时代,架构还需要为机器开发者提供一个清晰、有限、可验证的决策空间。
结语
我们一开始讨论的,是 JSX 能否像 KMP 一样,成为连接多个平台 UI 的共享层。
落到现实项目后,问题逐渐变成:
Next.js 和 React Native 之间应该共享什么?答案指向了产品状态、用户意图、业务规则、平台策略和验收场景。
继续走向 Agent First 后,问题又升级成:
Agent 应该根据什么来实现四端产品?最终,共享层可能会继续向上生长。
从共享组件,走向共享 Controller。
从共享 Controller,走向共享产品模型。
从共享产品模型,走向 Product IR 和可验证的 Agent 工作流。
到那个阶段,我们维护的已经不只是一个多端代码仓库。
它更像一套产品编译系统。
输入是产品目标、规则、平台能力和验收标准。
输出是 Web、H5、iOS、Android,以及对应的测试、埋点、文档和发布证据。
JSX 依然重要。
React DOM 和 React Native 依然重要。
但它们会越来越接近产品系统的渲染后端。
真正长期稳定的核心,是那套能够被人理解、被 Agent 执行、被测试验证的产品语义。
