
一句话:React Native 不是「把 React 跑在浏览器里」,而是「换掉渲染器(react-dom → 原生组件)+ 换掉宿主环境(DOM → Native)」——所以「React 的心智模型(组件/状态/Hooks/单向数据流)完全通用」,但「没有 DOM、没有 CSS、没有 URL」意味着 Web 的很多习惯必须换掉;而 Expo 是「把 RN 的原生工程复杂度封装掉」的工具链。
一、本质:同一个协调器,不同的「宿主配置」
⭐⭐⭐ RN 与 react-dom 的关系(理解这一点,后面全都顺):
React 的架构分两层:
① **reconciler**(协调器)——「diff、Fiber、优先级、Hooks、Suspense」
⭐ 这一层是**共享的**(`react-reconciler` 包)
② **host config**(宿主配置)——「怎么把变更落到具体平台」
· react-dom → 操作 DOM 节点
· react-native → 操作原生视图(UIView / android.view.View)
· react-three-fiber → 操作 WebGL 场景
· react-pdf / react-test-renderer → 各自的宿主
⭐ 类比:一个「虚拟 DOM 的操作系统内核」+ 「多个驱动」。
RN 就是「把驱动换成原生视图的那个 React」。
⭐ 所以:
✅ 通用的:组件/Props/State/Hooks/Context/Suspense/错误边界/单向数据流/不可变更新
❌ 不通用的:DOM API、CSS、`<div>`、`localStorage`、URL/History、`window`
⭐⭐⭐ RN 架构演进(面试高频,也是「为什么现在更快」的答案):
【旧架构(Bridge)】
JS 线程 ──(异步 JSON 消息)── Bridge ──→ Native 线程
· ⭐ **异步**:JS 无法「同步读」原生值(如「读取当前滚动位置」要回调)
· ⭐ **序列化开销**:每次通信都要 JSON 序列化/反序列化
· ⭐ **队列拥堵**:Bridge 是单队列,密集通信会「排队」(掉帧的常见原因)
· 三个线程:JS / Shadow(算布局)/ Native(UI)
【新架构(0.76 起默认启用)】
· **JSI(JavaScript Interface)**⭐ 核心
JS 引擎可以直接持有 **C++ 对象的引用**,**同步调用**、**无需序列化**
→ 让「同步读原生值」「直接用 C++ 写的高性能库」成为可能
· **Fabric(新渲染器)**
C++ 实现;支持「同步布局」、「中断式渲染」、优先级(⭐ 与 React 并发特性对齐)
· **TurboModules**
新的原生模块系统:**懒加载**(用到才加载)+ 走 JSI(更快)
· **Codegen**
根据 TypeScript 类型「生成原生接口代码」→ ⭐ 「类型安全的原生模块」(不再手写桥接样板)
· **Hermes**(默认 JS 引擎)
为 RN 优化:启动快、内存小、**支持「字节码预编译」**(发布时不带 JS 源码)
· **Bridgeless**
完全移除 Bridge(新架构的最终形态)
⭐ 一句话总结收益:「**同步调用 + 更少序列化 + 可中断渲染 + 更快启动**」。
二、与 Web 的核心差异:一张表 + 五段代码
| 维度 | Web(react-dom) | React Native |
|---|---|---|
| 基础元素 | div/span/img/a |
View/Text/Image/Pressable |
| 样式 | CSS(选择器 + 级联) | ⭐ JS 对象 + StyleSheet.create(无级联、无选择器) |
| 布局 | CSS 布局(含 grid/float) |
Yoga(Flexbox 子集),⚠️ flexDirection 默认 column |
| 单位 | px/rem/em/% |
⭐ 无单位数字(dp);% 支持但父容器必须有确定尺寸 |
| 滚动 | 页面天然滚动 | ⭐ ScrollView/FlatList(没有「页面滚动」) |
| 列表 | 自己实现 / 虚拟滚动库 | FlatList/SectionList/FlashList(内建虚拟化) |
| 导航 | URL + History | React Navigation(Stack/Tab/Drawer) |
| 存储 | localStorage/IndexedDB |
AsyncStorage/MMKV/expo-sqlite |
| 网络 | fetch(自动带 cookie) |
fetch(⚠️ 无 cookie,token 要自己管) |
| 动画 | CSS/WAAPI | Animated / Reanimated(可在 UI 线程) |
| 无障碍 | aria-* |
accessibilityLabel/accessibilityRole(⭐ 新版也支持 aria-*) |
| 文本 | 任意元素可含文本 | ⚠️ 文本必须包在 <Text> 里(否则报错) |
| 阴影 | box-shadow |
⚠️ iOS 用 shadow*,Android 用 elevation(新版支持 boxShadow) |
| 调试 | DevTools | Flipper / React DevTools / Metro 日志 |
| 热更新 | HMR(模块级) | Fast Refresh(组件级) |
| 发布 | 部署静态文件 | ⭐ 打包成 app(App Store/Play)+ OTA 更新 JS |
// ✅ 差异 1:基础元素与样式写法
// Web
<div style={{ display: 'flex', flexDirection: 'row', gap: 8, padding: 16 }}>
<span style={{ fontWeight: 'bold', color: '#1677ff' }}>标题</span>
</div>
// React Native
import { View, Text, StyleSheet } from 'react-native';
<View style={styles.row}>
<Text style={styles.title}>标题</Text>
</View>
const styles = StyleSheet.create({
row: {
// ⚠️ RN 里 flex 是「默认开启」的,且 flexDirection 默认是 'column'
flexDirection: 'row',
gap: 8, // ⭐ RN 0.71+ 支持 gap
padding: 16, // ⚠️ 无单位(不是 '16px')
},
title: {
fontWeight: 'bold',
color: '#1677ff',
// ⚠️ 没有「继承」:字号/颜色必须写在使用它的 Text 上
},
});
// ✅ 差异 2:布局的「两个致命差异」
// ① flexDirection 默认 column(Web 默认 row)
// ② 「百分比高度」需要「父容器有确定尺寸」
// ❌ 这段在 RN 里「什么都不显示」
<View style={{ flex: 1 }}>
<View style={{ height: '50%' }} /> {/* ⚠️ 父容器没有明确高度的话,% 无效 */}
</View>
// ✅ 正确:用 flex 或者给父容器明确尺寸
<View style={{ flex: 1 }}>
<View style={{ flex: 1 }} /> {/* 占满剩余空间 */}
<View style={{ flex: 1 }} />
</View>
// ✅ 或者用 Dimensions / useWindowDimensions 拿到具体数值
import { useWindowDimensions } from 'react-native';
function HalfScreen() {
const { height } = useWindowDimensions();
return <View style={{ height: height / 2 }} />;
}
// ✅ 差异 3:文本必须包在 <Text> 里(且 Text 不能有 View 子节点)
// ❌ 报错:Text strings must be rendered within a <Text> component
<View>Hello</View>
// ✅
<View><Text>Hello</Text></View>
// ⚠️ 反过来也不行:Text 里不能直接放 View
<Text><View /></Text> // ❌
// ✅ Text 里可以嵌套 Text(用于「部分加粗/变色」)
<Text>共 <Text style={{ fontWeight: 'bold' }}>3</Text> 项</Text>
// ✅ 差异 4:列表(RN 内建虚拟化)
import { FlatList } from 'react-native';
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Row item={item} />}
// ⭐ 性能相关(RN 里比 Web 更需要注意,因为「渲染」成本更高)
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={7}
removeClippedSubviews // ⚠️ Android 上有效,但可能有「空白」问题
getItemLayout={(_, index) => ({ length: 56, offset: 56 * index, index })} // ⭐ 定高时大幅提速
onEndReached={loadMore}
onEndReachedThreshold={0.5}
ListEmptyComponent={<Empty />}
ListFooterComponent={loading ? <Spinner /> : null}
/>
// ✅ 差异 5:动画(Reanimated 可以让动画「完全跑在 UI 线程」)
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function Draggable() {
// ⭐ shared value:存在于「UI 线程」的值(JS 线程读不到实时值)
const offset = useSharedValue({ x: 0, y: 0 });
const style = useAnimatedStyle(() => ({
// ⭐ 这段「worklet」在 UI 线程执行 → 即使 JS 线程卡顿,动画依然流畅
transform: [{ translateX: offset.value.x }, { translateY: offset.value.y }],
}));
return (
<Animated.View style={style} />
);
}
// ⭐ 这与 Web 的「合成线程动画」是同一个思想(见 5.17):
// 把「每帧的更新」从「主(JS)线程」搬到「渲染线程」
三、Expo:把原生复杂度封装掉
⭐⭐⭐ Expo 的四层能力(理解这个就能判断「什么时候需要 eject」):
① 【Expo SDK】
一大套「开箱可用的原生能力」的 JS API:
expo-camera / expo-location / expo-notifications / expo-image / expo-router …
⭐ 好处:不用「找库 + 手动链接原生代码 + 配 Xcode/Gradle」
② 【EAS Build / EAS Update】
· **EAS Build**——⭐ **云端构建**(不用本地装 Xcode/Android Studio)
· 支持「凭据管理」「多环境(dev/preview/prod)」「CI 集成」
· **EAS Update**——⭐ **OTA 更新 JS bundle**
⚠️ **只能更新 JS/资源,不能更新「原生代码」**(原生变了必须重新发版)
⭐ 能力:按「渠道/版本/用户」灰度发布 JS(见 8.9)
③ 【三种工作流】(决定「你能改多少原生代码」)
· **Expo Go**——用官方 App 跑你的 JS
✅ 零配置、秒级预览;⚠️ 只能用 SDK 里已有的原生模块(不能加自定义原生代码)
· **Development Build**(⭐ 现在的推荐)——构建「你自己的 App」但带 Expo 工具链
✅ 可以用任意原生库、有调试菜单、支持 Expo 的所有能力
· **Bare / Prebuild**——生成 `ios/` 与 `android/` 目录,完全自己管
✅ 最大控制权;⚠️ 升级 RN/Expo 版本要自己处理原生差异
⭐ 关键概念:**Config Plugins**——「用 JS 声明式地修改原生配置」
(如「加一个权限」「改 Info.plist」),这样「不 eject 也能定制原生」
④ 【Expo Router】
文件路由(与 Next.js 类似):
app/index.tsx → /
app/about.tsx → /about
app/user/[id].tsx → /user/:id
app/(tabs)/_layout.tsx → 标签导航
⭐ 好处:「路由」变成「文件结构」,且支持「深链接(Deep Link)自动映射」
⭐⭐⭐ 「什么时候需要自定义原生代码」(Expo 上最常见的决策):
需要 → 用 **Development Build**(或 Prebuild):
· 用了「Expo SDK 之外的原生库」(如第三方 SDK:支付/地图/推送/统计)
· 需要「自定义原生权限/配置」(用 **Config Plugin** 声明式地加)
· 需要「写原生模块」(用 **TurboModule + Codegen**)
· 需要「改启动图/图标/签名」
⭐ 反过来:如果你「只用 Expo SDK 里的能力」→ **Expo Go 就能开发**
(但要发布上线,仍然建议 Development Build:更接近真实环境)
⚠️ 一个常见误解:「用了 Expo 就不能自定义原生」
❌ 不对——**Config Plugins + Development Build** 让「不 eject 也能定制」。
真正「不能」的是「Expo Go 里跑自定义原生代码」。
四、完整实现:一套「Web 与 RN 共享代码」的策略(230 行)
⭐⭐⭐ 共享策略的三层(从「完全共享」到「完全分叉」):
① 【100% 共享】纯逻辑(无平台 API)
数据转换、校验、格式化、状态机、业务规则、类型定义
⭐ 这些「与渲染无关」,两端完全一样
② 【接口共享、实现分叉】平台适配层
· 用「平台特定文件」:`Button.native.tsx` / `Button.web.tsx`
· 用「适配接口」:`getStorage()` / `showToast()` / `navigate()`
· 用 `Platform.select({ ios, android, web, default })`
③ 【完全分叉】真正平台专属的东西
· 原生权限、推送、蓝牙、生物识别、文件系统路径
· 平台特有的交互(长按菜单、Swipe 手势、Tab bar 样式)
⭐ 结论:「能共享逻辑就共享逻辑,该分叉就大方分叉」——
不要为了「代码统一」把两端的体验都做差
// ✅ 层 1:纯逻辑共享(packages/shared/)
// ⭐ 关键:不 import 任何平台 API(不引 react-dom、不引 react-native)
export interface CartItem {
id: string;
name: string;
price: number;
qty: number;
}
/** ⭐ 纯函数:两端完全一样(可单测) */
export function calcCartTotal(items: CartItem[], coupon: string | null): number {
const sum = items.reduce((acc, i) => acc + i.price * i.qty, 0);
if (coupon === 'HALF') return sum / 2;
if (coupon === 'MINUS10') return Math.max(0, sum - 10);
return sum;
}
/** ⭐ 业务校验也共享(表单两端同一套规则) */
export function validateEmail(email: string): string | null {
if (!email) return '邮箱必填';
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return '邮箱格式不正确';
return null;
}
/** ⭐ 状态机也共享(用 useReducer 的 reducer 是纯函数) */
export function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'add': {
const existing = state.items.find((i) => i.id === action.item.id);
return existing
? { ...state, items: state.items.map((i) => (i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i)) }
: { ...state, items: [...state.items, { ...action.item, qty: 1 }] };
}
case 'remove':
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
default:
return state;
}
}
// ✅ 层 2:平台适配层(定义接口,两端各自实现)
// packages/shared/adapters.ts —— ⭐ 只定义「接口」,不实现
export interface PlatformAdapters {
storage: {
get: (key: string) => Promise<string | null>;
set: (key: string, value: string) => Promise<void>;
remove: (key: string) => Promise<void>;
};
toast: {
show: (message: string, type?: 'info' | 'error') => void;
};
haptic: {
light: () => void;
};
/** 打开外部链接(Web 用 window.open / location;RN 用 Linking) */
openUrl: (url: string) => Promise<void>;
/** 读取「安全区」(RN 有刘海/底部横条;Web 为 0) */
getSafeAreaInsets: () => { top: number; bottom: number; left: number; right: number };
}
// ⭐ 用一个 Context 注入(而不是「直接 import 平台代码」)
import { createContext, useContext } from 'react';
export const AdaptersContext = createContext<PlatformAdapters | null>(null);
export function useAdapters(): PlatformAdapters {
const ctx = useContext(AdaptersContext);
if (!ctx) throw new Error('AdaptersContext 未提供:请在应用根部注入平台适配器');
return ctx;
}
// ✅ 层 2 的实现(Web 侧)
// apps/web/src/adapters.ts
import type { PlatformAdapters } from '@acme/shared/adapters';
export const webAdapters: PlatformAdapters = {
storage: {
get: async (key) => localStorage.getItem(key),
set: async (key, value) => localStorage.setItem(key, value),
remove: async (key) => localStorage.removeItem(key),
},
toast: {
show: (message, type) => {
// 用现有的 Toast 组件(见 11.15)
window.dispatchEvent(new CustomEvent('toast', { detail: { message, type } }));
},
},
haptic: {
// ⭐ Web 用 Vibration API(支持有限)
light: () => navigator.vibrate?.(10),
},
openUrl: async (url) => { window.open(url, '_blank', 'noopener,noreferrer'); },
getSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
};
// ✅ 层 2 的实现(RN 侧)
// apps/mobile/src/adapters.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Haptics from 'expo-haptics';
import * as Linking from 'expo-linking';
import { Platform, ToastAndroid, Alert } from 'react-native';
import { initialWindowMetrics } from 'react-native-safe-area-context';
import type { PlatformAdapters } from '@acme/shared/adapters';
export const nativeAdapters: PlatformAdapters = {
storage: {
get: (key) => AsyncStorage.getItem(key),
set: (key, value) => AsyncStorage.setItem(key, value),
remove: (key) => AsyncStorage.removeItem(key),
},
toast: {
show: (message, type) => {
// ⭐ 平台差异:Android 有 ToastAndroid,iOS 只能自己做一个 toast 组件
if (Platform.OS === 'android') {
ToastAndroid.show(message, ToastAndroid.SHORT);
} else {
// ⭐ iOS 上用「自研 toast」或 Alert(注意:Alert 会打断用户)
if (type === 'error') Alert.alert('提示', message);
else useIosToastStore.getState().show(message); // 自研
}
},
},
haptic: {
light: () => { void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); },
},
openUrl: (url) => Linking.openURL(url),
getSafeAreaInsets: () => ({
top: initialWindowMetrics?.insets.top ?? 0,
bottom: initialWindowMetrics?.insets.bottom ?? 0,
left: initialWindowMetrics?.insets.left ?? 0,
right: initialWindowMetrics?.insets.right ?? 0,
}),
};
// ⚠️ 上面的 useIosToastStore 是示意——真实项目会放一个 toast 组件
const useIosToastStore = { getState: () => ({ show: (_m: string) => {} }) };
// ✅ 层 2:用「包装组件」共享 UI 逻辑,让「平台特定文件」只负责渲染
// packages/shared/ui/Button.tsx —— ⭐ 共享的「逻辑与 props 契约」
import { useAdapters } from '../adapters';
export interface ButtonProps {
variant?: 'primary' | 'default' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
onPress: () => void;
children: string;
}
/** ⭐ 共享的「业务逻辑」:禁用条件、加载时阻止重复点击、触感反馈 */
export function useButtonBehavior({ loading, disabled, onPress }: Pick<ButtonProps, 'loading' | 'disabled' | 'onPress'>) {
const { haptic } = useAdapters();
const isDisabled = disabled || loading;
const handlePress = () => {
if (isDisabled) return;
// ⭐ 移动端「触感反馈」是体验的一部分(Web 上通常是 no-op)
haptic.light();
onPress();
};
return { isDisabled, handlePress };
}
// Button.native.tsx —— ⭐ Metro 会「优先选 .native.tsx」
import { Pressable, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { useButtonBehavior, type ButtonProps } from '@acme/shared/ui/Button';
export function Button({ variant = 'default', size = 'md', loading, disabled, onPress, children }: ButtonProps) {
const { isDisabled, handlePress } = useButtonBehavior({ loading, disabled, onPress });
return (
<Pressable
onPress={handlePress}
disabled={isDisabled}
// ⭐ 无障碍:RN 用 accessibility* 属性(新版也支持 aria-*)
accessibilityRole="button"
accessibilityState={{ disabled: isDisabled, busy: loading }}
accessibilityLabel={children}
style={({ pressed }) => [
styles.base,
styles[size],
styles[variant],
pressed && styles.pressed, // ⭐ RN 的「按压态」由 Pressable 提供
isDisabled && styles.disabled,
]}
>
{loading ? <ActivityIndicator size="small" /> : <Text style={styles.label}>{children}</Text>}
</Pressable>
);
}
const styles = StyleSheet.create({
base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 8, gap: 6 },
sm: { height: 28, paddingHorizontal: 10 },
md: { height: 36, paddingHorizontal: 14 },
lg: { height: 44, paddingHorizontal: 18 },
default: { backgroundColor: '#fff', borderWidth: 1, borderColor: '#d9d9d9' },
primary: { backgroundColor: '#1677ff' },
danger: { backgroundColor: '#f5222d' },
pressed: { opacity: 0.86 }, // ⭐ 没有 :hover/:active,用 pressed
disabled: { opacity: 0.5 },
label: { fontSize: 14, color: '#1f1f1f' },
});
// Button.web.tsx —— 同一份 props 契约,Web 实现
import { useButtonBehavior, type ButtonProps } from '@acme/shared/ui/Button';
import './Button.css';
export function Button({ variant = 'default', size = 'md', loading, disabled, onPress, children }: ButtonProps) {
const { isDisabled, handlePress } = useButtonBehavior({ loading, disabled, onPress });
return (
<button
type="button"
className={`btn btn--${variant} btn--${size} ${loading ? 'btn--loading' : ''}`}
disabled={isDisabled}
aria-busy={loading || undefined}
onClick={handlePress}
>
{loading && <span className="btn__spinner" aria-hidden />}
<span className="btn__content">{children}</span>
</button>
);
}
// ✅ 层 2:共享的 design token(Web 用 CSS 变量 / RN 用 JS 对象)
// ⭐ 从「同一份源」生成两种产物(见 10.13 的 token 生成器)
// packages/tokens/src/tokens.ts —— 原始值(两端都可读)
export const rawTokens = {
colorPrimary: '#1677ff',
spaceSm: 4,
spaceMd: 8,
spaceLg: 16,
radiusMd: 8,
fontSizeSm: 14,
} as const;
// packages/tokens/src/native.ts —— RN 用「JS 对象」
export const theme = {
color: { primary: rawTokens.colorPrimary, text: '#1f1f1f' },
space: { sm: rawTokens.spaceSm, md: rawTokens.spaceMd, lg: rawTokens.spaceLg },
radius: { md: rawTokens.radiusMd },
fontSize: { sm: rawTokens.fontSizeSm },
// ⭐ RN 没有 CSS 变量 → 主题切换要靠「Context 提供一个新的 theme 对象」
} as const;
// packages/tokens/src/tokens.css —— Web 用 CSS 变量(由生成器产出)
// :root { --color-primary: #1677ff; --space-md: 8px; }
/* ✅ 层 2:共享的「响应式断点」也要「各自表述」 */
/* Web:媒体查询 */
/* @media (min-width: 768px) { .card { flex-direction: row; } } */
/* RN:用 useWindowDimensions 判断(没有媒体查询 CSS) */
// ✅ RN 侧的「响应式」写法
import { useWindowDimensions } from 'react-native';
function useBreakpoint() {
const { width } = useWindowDimensions();
// ⭐ 与 Web 的断点保持一致(从 token 里读)
if (width < 768) return 'sm';
if (width < 1024) return 'md';
return 'lg';
}
function Card() {
const bp = useBreakpoint();
return (
<View style={{ flexDirection: bp === 'sm' ? 'column' : 'row', gap: theme.space.md }}>
{/* ... */}
</View>
);
}
⭐⭐⭐ 这套策略的五个「关键决定」:
① 【纯逻辑 100% 共享】⭐ 收益最大
校验/计算/状态机/类型 → 两端零重复(也不会「Web 改了规则 App 没改」)
判据:「**这段代码 import 了平台 API 吗**」——没有就共享。
② 【用「接口 + Context 注入」而不是「条件 import 平台代码」】
❌ `import { Platform } from 'react-native'` 出现在共享代码里
✅ 共享代码只依赖「接口」,由各端注入实现
⭐ 好处:共享包**不依赖任何平台**(可以单独单测,也不会「Web 打包时把 RN 打进来」)
③ 【平台特定文件(`Button.native.tsx` / `Button.web.tsx`)】
Metro(RN)与 Vite/webpack 都支持「平台后缀优先」解析
⭐ 好处:**同一份 props 契约、两份渲染实现**——
共享的是「接口与行为逻辑」,分叉的是「渲染原语」。
④ 【「行为逻辑」提炼成 Hook(`useButtonBehavior`)】⭐
这样「禁用条件、防重复点击、触感反馈」只写一次,
两端只是「用不同的原语渲染」。
⭐ 这是「跨平台共享」最有效的模式:**逻辑共享 + 渲染分叉**。
⑤ 【token 从「同一份源」生成两种产物】
Web → CSS 变量(可主题切换);RN → JS 对象(+ Context 做主题)
⭐ 避免「两边的品牌色慢慢不一致」
五、本篇特有的坑
// ① 文本没有包在 <Text> 里
<View>Hello</View> // ❌ 直接报错
// ✅ <View><Text>Hello</Text></View>
// ② 在共享代码里 import 了 react-native(Web 打包会炸)
// packages/shared/hooks/useFoo.ts
import { Platform } from 'react-native'; // ❌ 共享包被污染
// ✅ 抽成「接口」,由各端注入
// ③ 以为 `flexDirection` 默认是 row(Web 习惯)
<View style={{ flex: 1 }}> // ⚠️ RN 默认 column
// ✅ 显式写 flexDirection
// ④ 用「百分比高度」但没有「确定高度的父容器」
<View><View style={{ height: '50%' }} /></View> // ⚠️ 无效
// ✅ 用 flex 或 useWindowDimensions
// ⑤ 把「px 字符串」写进 RN 样式
{ padding: '16px' } // ❌ RN 只接受数字
// ✅ { padding: 16 }
// ⑥ 期望「样式继承」(如 body 的 fontFamily 传下去)
// ⚠️ RN 里「文本样式不继承」(除了「Text 套 Text」)
// ✅ 自己封装一个 <AppText> 或在 theme 里统一
// ⑦ 用 `box-shadow` 但两端表现不同
// ⚠️ iOS 用 shadowColor/shadowOffset/shadowOpacity/shadowRadius;
// Android 只支持 elevation(没有颜色/偏移的精细控制)
// ✅ 用「库封装」(如 react-native-shadow-2)或「接受差异」
// ⑧ 用「`position: fixed` 做浮层」(RN 里没有 fixed)
// ✅ 用「绝对定位 + 父容器占满屏幕」,或库(Modal / Portal 实现)
// ⑨ 用「CSS 媒体查询」做响应式
// ✅ useWindowDimensions + 断点常量(与 Web 共享断点值)
// ⑩ 用「DOM 事件」的思维做手势(onMouseEnter 等)
// ✅ RN 用 Pressable 的 pressed/hovered 状态,或 Gesture Handler
// ⑪ 忽略「安全区」(刘海屏/全面屏底部横条)
// ✅ react-native-safe-area-context 的 SafeAreaView/useSafeAreaInsets
// ⑫ 用「`window`/`document`/`localStorage`」等 Web API
// ✅ 通过「适配层」(见第四节)
// ⑬ 「网络请求」以为会自动带 cookie(RN 里没有 cookie 罐)
// ✅ 用 token + 显式 header;或 cookies 库
// ⑭ 直接在 RN 里用「大数据量的 SectionList/FlatList」但不做性能优化
// ⚠️ RN 的「渲染成本」比 Web 高(要跨 JSI/原生)
// ✅ getItemLayout(定高)+ 合理的 windowSize + 「避免内联函数/对象」
// (⭐ 与 Web 的 React 优化是同一套思路,见 5.4/5.8)
// ⑮ 「热更新」当作「发了新版本」(EAS Update 只能更新 JS)
// ⚠️ 「原生代码变了」必须重新发版(App Store/Play 审核)
// ✅ 把「原生依赖的变更」与「JS 变更」分开发布
// ⑯ 用「Expo Go」开发但用了「自定义原生库」(跑不起来)
// ✅ 改用 Development Build
// ⑰ 升级 Expo SDK 时「直接改版本号」(原生依赖不匹配)
// ✅ 用 `npx expo install` 装包(它会「选与当前 SDK 兼容的版本」)⭐
// ⚠️ 不要用 `npm install <pkg>`(会装最新版,可能与 SDK 不兼容)
// ⑱ 用「Web 的图片方案」(图片没有显式尺寸)
<Image source={{ uri }} /> // ⚠️ RN 里「必须给尺寸」否则不显示
// ✅ <Image source={{ uri }} style={{ width: 300, height: 200 }} />
// ⭐ 或用 expo-image 的 contentFit/placeholder(更现代)
// ⑲ 「动画」用 JS 线程的 Animated(掉帧)
// ⚠️ 如果动画涉及「JS 线程的计算」,主线程卡就掉帧
// ✅ 用 Reanimated(worklet 在 UI 线程)
// ⑳ 「共享代码」里用了 Web 独有的库(如 react-dom、CSS-in-JS 的 Web 实现)
// ⚠️ 打包会报错或体积暴增
// ✅ 共享包里「零平台依赖」(可用 ESLint `no-restricted-imports` 强制)
// ⑳ 的完整处理(用 ESLint 强制「共享包不许引平台代码」)
// packages/shared/eslint.config.js
export default [
{
files: ['src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': ['error', {
paths: [
{ name: 'react-native', message: '共享包不能依赖 react-native(请通过适配层接口)' },
{ name: 'react-dom', message: '共享包不能依赖 react-dom' },
],
patterns: [
{ group: ['react-native-*', 'expo-*'], message: '共享包不能依赖 RN/Expo 的库' },
],
}],
// ⭐ 也不允许用「平台全局变量」
'no-restricted-globals': ['error',
{ name: 'window', message: '共享代码不能直接用 window(用适配层)' },
{ name: 'document', message: '共享代码不能直接用 document(用适配层)' },
{ name: 'localStorage', message: '共享代码不能直接用 localStorage(用适配层)' },
],
},
},
];
// ⑪ 的完整处理(安全区)
// app/_layout.tsx(Expo Router)
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
export default function RootLayout() {
return (
<SafeAreaProvider>
{/* ⭐ edges 指定「哪些边要避让」(不是所有页面都需要避让顶部) */}
<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }} edges={['top', 'bottom']}>
<Slot />
</SafeAreaView>
</SafeAreaProvider>
);
}
// ⭐ 需要「局部避让」时用 Hook:
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function BottomBar() {
const insets = useSafeAreaInsets();
return <View style={{ paddingBottom: insets.bottom + 8 }} />;
}
// ⑰ 的完整处理(用 expo install 保证版本兼容)
{
"scripts": {
"add": "npx expo install",
"//": "⭐ 用 `npm run add expo-camera` 而不是 `npm install expo-camera`",
"doctor": "npx expo-doctor",
"//2": "⭐ expo-doctor 会检查「依赖版本与 SDK 是否匹配」"
}
}
# ⭐ 升级 Expe SDK 的标准流程
npx expo install expo@latest
npx expo install --fix # 把所有包「修到与当前 SDK 兼容的版本」
npx expo-doctor # 检查问题
npx expo start --clear # 清缓存重启
六、面试延伸
- 「React Native 是什么?和 React 什么关系?」
React Native 不是「把 React 跑在浏览器里」,而是「换掉渲染器与宿主环境」。React 的架构分两层:① reconciler(协调器)——负责 diff、Fiber、优先级、Hooks、Suspense;② host config(宿主配置)——负责「把变更落到具体平台」。⭐ reconciler 是共享的(react-reconciler),RN 只是把 host config 从「操作 DOM」换成「操作原生视图」(UIView / android.view.View)。所以:组件、Props、State、Hooks、Context、Suspense、错误边界、单向数据流、不可变更新完全通用;而 DOM API、CSS、<div>、localStorage、URL/History、window都不存在。(同一个机制也解释了 react-three-fiber / react-pdf 这类库的存在。) -
「React Native 的新架构(New Architecture)带来了什么?」
核心是 JSI 带来的「同步调用 + 无序列化」:① ⭐ JSI(JavaScript Interface)——JS 引擎可以直接持有 C++ 对象的引用,同步调用(旧 Bridge 是异步 JSON 消息),于是「同步读原生值」「直接用 C++ 写的高性能库」成为可能,也消除了序列化开销与桥接队列拥堵;② Fabric——C++ 实现的新渲染器,支持同步布局、可中断渲染、优先级(⭐ 与 React 的并发特性对齐);③ TurboModules——新原生模块系统,懒加载 + 走 JSI;④ Codegen——根据 TypeScript 类型生成原生接口代码(类型安全,不用手写桥接样板);⑤ Hermes——为 RN 优化的 JS 引擎(启动快、内存小、字节码预编译);⑥ Bridgeless——完全移除 Bridge。 -
「RN 和 Web 开发最需要注意哪些差异?」
六类:① ⭐ 没有 DOM/CSS——用View/Text/Image,样式是 JS 对象 +StyleSheet.create(无选择器、无级联、无继承);② ⭐ 布局是 Yoga(Flexbox 子集)——flexDirection默认column(Web 默认row),没有grid/float,百分比高度需要父容器有确定尺寸;③ ⭐ 文本必须包在<Text>里(且Text不能有View子节点);④ 没有「页面滚动」——用ScrollView/FlatList(列表内建虚拟化);⑤ 没有 URL/History——用 React Navigation(或 Expo Router 的文件路由);⑥ 没有window/document/localStorage——用 适配层(AsyncStorage/MMKV等)。此外还有「单位无 px」「阴影两端不同」「动画用 Animated/Reanimated」「无障碍用accessibility*」等。 -
「RN 里怎么做动画才不卡?」
核心是「把每帧的更新从 JS 线程搬到 UI 线程」——⭐ 这与 Web 的「合成线程动画」是同一思想(见 5.17)。①AnimatedAPI 的useNativeDriver: true可以把transform/opacity动画交给原生驱动(但布局属性不支持原生驱动);② ⭐ Reanimated——用useSharedValue+useAnimatedStyle写「worklet」,worklet 在 UI 线程执行,所以「即使 JS 线程被卡住,动画依然流畅」(⭐ 这是 RN 里做手势/拖拽/复杂动画的主流方案);③ 避免「动画里做 JS 重计算」;④ 手势用 Gesture Handler(跑在 UI 线程)。 -
「Expo 是什么?什么时候需要自定义原生代码?」
Expo 是「把 RN 的原生工程复杂度封装掉」的工具链,四层能力:① Expo SDK(开箱可用的原生能力 JS API:camera/location/notifications/image/router…);② EAS Build / EAS Update(云端构建 + OTA 更新 JS bundle,⚠️ 只能更新 JS/资源,原生代码变了必须重新发版);③ 三种工作流——Expo Go(零配置但只能用 SDK 内的原生模块)、⭐ Development Build(现在的推荐:自己的 App + Expo 工具链,可用任意原生库)、Bare/Prebuild(生成ios//android/,最大控制权);④ Expo Router(文件路由 + 深链接自动映射)。需要自定义原生代码时:用 Development Build(或 Prebuild),并通过 ⭐ Config Plugins「声明式地修改原生配置」(如加权限、改Info.plist)——⭐ 所以「用了 Expo 就不能自定义原生」是误解,真正不能的是「在 Expo Go 里跑自定义原生代码」。 -
「Web 与 RN 的代码怎么共享?」
三层策略:① ⭐ 纯逻辑 100% 共享(校验、计算、状态机的 reducer、类型定义)——收益最大,判据是「这段代码 import 了平台 API 吗」;② ⭐ 接口共享、实现分叉——定义PlatformAdapters接口(storage/toast/haptic/openUrl/safeArea…),用 Context 注入各端实现(而不是「在共享代码里条件 import 平台代码」);③ ⭐ UI 用「平台特定文件」+ 「行为逻辑提炼成 Hook」——Button.native.tsx/Button.web.tsx共享同一份 props 契约,而「禁用条件、防重复点击、触感反馈」抽成useButtonBehavior(逻辑共享、渲染分叉)。另外:design token 从「同一份源」生成两种产物(Web → CSS 变量;RN → JS 对象)。原则:「能共享逻辑就共享,该分叉就大方分叉」——不要为了「代码统一」把两端体验都做差。 -
「(如果做过)RN 里最容易踩的坑是什么?」
六个高频:① ⭐ 文本没包<Text>(直接报错);② ⭐flexDirection默认column(Web 习惯会写错布局);③ 百分比高度失效(父容器没有确定尺寸);④ 样式写'16px'(RN 只接受数字);⑤ 共享代码里import了react-native/window/localStorage(Web 打包会炸,⭐ 用 ESLint 的no-restricted-imports/no-restricted-globals强制);⑥ ⭐ 用npm install装 Expo 包(版本可能与 SDK 不兼容,必须用npx expo install,它会选兼容版本;升级时用expo install --fix+expo-doctor检查)。 -
「RN 的性能优化和 Web 有什么异同?」
相同的部分(因为 reconciler 一样):① 减少重渲染——memo、稳定 props 引用、状态下移、children透传(见 5.4/5.8);② 列表虚拟化(Web 用库,RN 内建FlatList);③ 避免「渲染中做重计算」(useMemo);④ ⭐ 「只改transform/opacity的动画更快」(Web 走合成线程,RN 走 UI 线程/原生驱动)。不同的部分:① ⭐ RN 的「渲染成本」更高(要跨 JSI/原生创建视图)→ 所以FlatList的getItemLayout、windowSize、initialNumToRender这些参数比 Web 更关键;② ⭐ 列表项里的「内联函数/对象」代价更大(会触发更多原生更新)→useCallback/useMemo在 RN 里更值得用;③ 动画要用 Reanimated /useNativeDriver(Web 用 CSS);④ 启动时间(Hermes 字节码 + 延迟初始化);⑤ ⭐removeClippedSubviews、InteractionManager这类 RN 特有手段。
一句话速记
RN 的本质是「共享 reconciler(diff/Fiber/Hooks/Suspense)、替换 host config(DOM → 原生视图)」——所以 React 心智模型通用,但没有 DOM/CSS/URL/window;新架构的核心是 JSI(同步调用 + 无序列化,替代异步 Bridge) + Fabric(C++ 渲染器,支持同步布局与可中断渲染) + TurboModules(懒加载) + Codegen(类型生成桥接) + Hermes(字节码预编译) + Bridgeless;六大 Web→RN 差异:
View/Text/Image替代 DOM、样式是 JS 对象(无选择器/无级联/无继承)、Yoga 布局(⚠️flexDirection默认column、无grid、百分比高度需父容器确定尺寸)、文本必须包<Text>、列表用FlatList(内建虚拟化)、没有 URL(用 React Navigation)、没有window/localStorage(用适配层);动画不卡的关键是「搬到 UI 线程」(useNativeDriver/ ⭐ Reanimated 的 worklet,与 Web 的合成线程同源);Expo = 「封装原生复杂度的工具链」(SDK + EAS Build/Update ⚠️ OTA 只能更 JS + Development Build(推荐)/Bare + Expo Router),⭐ 「用了 Expo 就不能自定义原生」是误解(Config Plugins 可声明式改原生配置);共享策略三层:纯逻辑 100% 共享(判据:有没有 import 平台 API)→ 接口共享 + Context 注入实现(禁止在共享包里条件 import RN/DOM,用 ESLint 强制)→ UI 用平台特定文件 + 「行为逻辑提炼成 Hook」(逻辑共享、渲染分叉);⭐ 装 Expo 包必须用npx expo install(不是npm install)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!