启用 Web
npx expo install react-native-web react-dom @expo/metro-runtime
// app.json { "expo": { "web": { "bundler": "metro", // 默认 metro,能复用 RN 包 "output": "static", // static / server / single "favicon": "./assets/favicon.png" } } }
npx expo start --web # 或开着 expo start 然后按 w
三种 output 模式
| output | 产物 | 适用 |
|---|---|---|
| single | 单页 HTML,所有路由客户端渲染 | 内部工具、Demo |
| static | 每个路由预渲染一份 HTML | 营销站、文档、博客(SEO 友好) |
| server | Node 服务端渲染 + API 路由 | 需要登录态定制、Server Functions |
# 静态导出 npx expo export -p web # 产物在 dist/,扔 Vercel / CF Pages / 任何 static host
Platform-specific 文件
同一目录下,Expo 根据平台自动选对应后缀的文件:
components/ ├── Header.tsx ← 三端共用 ├── Header.web.tsx ← Web 专用(如需覆盖) ├── Map.native.tsx ← iOS + Android(不在 Web 上构建) └── Map.web.tsx ← Web(例如用 Mapbox GL JS)
// 调用时永远 import 裸名,Metro 自动匹配 import { Map } from './Map'; // native → Map.native.tsx // web → Map.web.tsx
Platform.OS 运行时判断
import { Platform } from 'react-native'; const styles = StyleSheet.create({ header: { paddingTop: Platform.OS === 'ios' ? 44 : 0, ...Platform.select({ web: { cursor: 'pointer' }, default: {}, }), }, });
路由差异
默认情况下 app/ 下的路由三端都能访问。但某些路由只在 Web 上存在(营销页、博客),可以用 .web.tsx:
app/ ├── (app)/ │ └── home.tsx ← 三端 ├── blog.web.tsx ← Web only └── marketing.web.tsx ← Web only
另一头,某些 Native only(比如蓝牙连接设置),可以 .native.tsx 或运行时 Platform.OS !== 'web' 守卫。
SEO:head 信息
import { Head } from 'expo-router/head'; export default function ProductDetail() { const { data } = useProduct(id); return ( <> <Head> <title>{data.name} - MyApp</title> <meta name="description" content={data.summary} /> <meta property="og:image" content={data.image} /> </Head> ... </> ); }
在 static/server 模式下 <Head> 会被注入到导出的 HTML,爬虫和分享预览都能读到。Native 端无副作用(忽略)。
Server Routes(beta)
app/
└── api/
└── hello+api.ts
// app/api/hello+api.ts —— 服务端接口 export async function GET(request: Request) { return Response.json({ msg: 'hello' }); }
// 前端调用 const data = await fetch('/api/hello').then((r) => r.json());
需要 output: 'server',部署到 Vercel/EAS Hosting。三端调用同样 URL,适合做轻量 BFF。
共享代码组织
src/ ├── api/ ← fetch 函数,三端用 ├── hooks/ ← 业务 hook ├── utils/ ← 纯逻辑 ├── components/ │ ├── ui/ ← 跨端 UI(Button/Input/Card) │ └── native/ ← 纯 Native 组件(相机/蓝牙) └── config/
规则:业务逻辑永远跨端,只有强依赖 native API 的放到 .native/.ios/.android 文件。
常见跨端坑
Shadow 样式
RN 的
shadowOffset/shadowOpacity Web 不支持,要加 elevation + boxShadow。用 Platform.select 分发。SafeAreaView
Web 没有刘海概念,
react-native-safe-area-context 在 Web 上 insets 返回 0,不用特殊处理。KeyboardAvoidingView
Web 下键盘没概念(桌面也没),条件渲染或直接用 flex。
第三方包没 Web 支持
比如
react-native-reanimated(有 Web 支持)、react-native-ble-plx(无)。查包文档的"Platforms"段落。字体加载
Web 用 CSS @font-face,Native 用 expo-font。可以写一个 useLoadedFonts hook,web 下立即 true。
跳转链接:Link 自动处理
<Link href="/products/42">详情</Link> // Web: 渲染成 <a href="/products/42">,支持右键新开、ctrl+点击 // Native: 渲染成 Pressable,点击 push 到下一屏
这是 Expo Router 的关键桥:同一个 Link 组件三端行为各自原生,URL 一致。
Web 特有:History / Pathname
import { usePathname, useRouter } from 'expo-router'; const router = useRouter(); router.push('/page'); // Web 调 history.pushState,Native 推屏 router.back(); // Web 调 history.back,Native 弹栈 const path = usePathname(); // 三端一致
打包部署
# 1. 静态导出 npx expo export -p web # 2. 产物 dist/ 推 Vercel vercel --prod dist # 3. 或 CF Pages: wrangler pages deploy dist # 4. 或纯 Nginx: rsync -avz dist/ root@server:/var/www/myapp/
Web-only 项目的选择
如果纯 Web,没必要用 Expo Router——Next.js 更合适。Expo Router 的甜区是「三端共用且 Mobile 为主」,Web 是锦上添花。
本章小结
- Web 通过
react-native-web把 RN 组件渲染到浏览器,Metro 打 Web bundle - output:
static预渲染、serverSSR + API、singleSPA - Platform-specific 文件
.web.tsx/.native.tsx/.ios.tsx/.android.tsx自动选 - Head/expo-router/head 管 SEO meta,
api/xxx+api.ts做服务端接口 - 跨端原则:业务逻辑共享,原生 API 隔离,Link/Platform.OS 做接口