4.3 Expo 與 React Native:跨平台的抽象層次#
跨平台的三代技術路線#
| 路線 | 代表 | 機制 | 取捨 |
|---|---|---|---|
| WebView 混合 | Cordova (2011) | 網頁包殼 | 效能/體驗差 |
| 編譯成原生碼 | Xamarin (2011)、Flutter (2017 自繪) | 各自編譯/自繪引擎 | 生態隔離 |
| 橋接原生元件 | React Native (2015) | JS 驅動原生 View | 平台一致感 + React 生態 |
React Native 的洞見:不要畫一個假的原生界面,直接使用真的原生元件。<Text> 映射到 iOS UITextView / Android TextView —— 使用者得到平台級的滾動、無障礙與字型渲染。
橋接架構的演化(因果鏈)#
timeline
title React Native 架構演化
2015 : Bridge — JSON 序列化跨橋,批次非同步
2018 : JSI 直接呼叫 — 去序列化,同步互通
2022 : Fabric 渲染器 + TurboModules — 延遲載入、類型安全
2024 : New Architecture 成為預設舊 Bridge 的瓶頸:每次通訊需 JSON 序列化,批次非同步導致 UI 高頻互動(滾動跟手)出現延遲。JSI(JavaScript Interface,2022 主導)讓 JS 直接持有 C++ 物件引用,同步呼叫、零序列化 —— 效能問題被架構修復而非調參修復。
Expo:從「工具集」到「平台」#
Expo 的分層抽象:
flowchart TB
subgraph 應用層
ER[expo-router\n檔案式路由]
EUI[expo UI 組件]
end
subgraph 能力層
EC[expo-camera / notifications / secure-store ...]
end
subgraph 建置層
EASB[EAS Build 雲端建置]
EASU[EAS Update OTA 更新]
DevC[Development Build + Dev Client]
end
應用層 --> 能力層 --> RN[React Native]
建置層 --> XC[Xcode / Gradle\n被抽象化]檔案式路由(expo-router)#
app/
├── _layout.tsx # 根布局(Provider、導航容器)
├── index.tsx # /
├── todos/
│ ├── index.tsx # /todos
│ └── [id].tsx # /todos/:id — 動態路由
└── login.tsx # /login
檔案結構即路由表 —— 與 Next.js 同源的約定,讓「URL 可分享、深度連結免設定」成為預設能力。
型別安全的導航#
import { Link, router } from 'expo-router';
// 宣告式跳轉,參數有型別
<Link href={{ pathname: '/todos/[id]', params: { id: todo.id } }}>
<TodoCard todo={todo} />
</Link>
// 命令式
router.push('/todos');
OTA 更新:原生 App 的「熱修復」能力#
EAS Update 讓 JS 層的修正繞過應用商店審核直接推送到裝置:
\[ \text{修復時間}: \text{商店審核(數天)} \Rightarrow \text{OTA(分鐘級)} \]
限制:僅限 JS/資源層;原生碼變更仍需重新建置上架 —— 抽象層的邊界就是更新能力的邊界。
網路層:與 Axum 後端整合#
// api/client.ts
const BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000';
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
...init,
headers: { 'Content-Type': 'application/json', ...init?.headers },
});
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json(); // TS 型別由 openapi 生成(見 4.4)
}
注意 EXPO_PUBLIC_ 前綴的環境變數會被內聯進 bundle —— 所以絕不能放機密;機密永遠只該在後端。
平台差異的處理策略#
import { Platform } from 'react-native';
const styles = StyleSheet.create({
header: {
paddingTop: Platform.OS === 'ios' ? 44 : 24, // 狀態列差異
...Platform.select({ ios: { fontFamily: 'Menlo' }, android: { fontFamily: 'monospace' } }),
},
});
原則:90% 程式碼共享,10% 平台分支。當平台分支超過 30%,應考慮把差異下沉到元件或原生模組,而不是讓業務代碼長滿 if (Platform.OS)。
習題#
- 畫圖比較舊 Bridge 與 JSI 的通訊路徑,說明序列化瓶頸消失的原因。
- 哪些變更可以用 EAS Update 推送?哪些不行?為什麼?
- 設計一個在 iOS/Android/Web 三端都可用的登入頁,標出需要平台分支的部分。