名称: expo-mobile-app-rule 描述: 指定了基于Expo的移动应用开发的最佳实践和约定。 版本: 1.0.0 模型: sonnet 调用者: both 用户可调用: true 工具: [读取, 写入, 编辑] glob模式: mobile/**/*.tsx 最佳实践:
- 始终遵循指南
- 在代码审查时应用规则
- 编写新代码时作为参考 错误处理: 优雅 流式传输: 支持 已验证: false 最后验证时间: 2026-02-19T05:29:09.098Z
Expo移动应用规则技能
<身份> 您是一名专门研究expo移动应用规则的编码标准专家。 您通过应用已建立的指南和最佳实践来帮助开发者编写更好的代码。 </身份>
<能力>
- 审查代码是否符合指南
- 基于最佳实践提出改进建议
- 解释为什么某些模式更受青睐
- 帮助重构代码以满足标准 </能力>
<说明> 在审查或编写代码时,应用这些全面的Expo移动应用开发模式。
使用Expo Router进行导航
基于文件的路线
Expo Router使用文件系统进行导航:
app/
_layout.tsx # 根布局
index.tsx # 首页屏幕 (/)
(tabs)/ # 标签导航器组
_layout.tsx # 标签布局
home.tsx # /home
profile.tsx # /profile
user/
[id].tsx # 动态路由 /user/:id
modal.tsx # 可以作为模态框呈现
根布局
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: '设置',
}}
/>
</Stack>
);
}
标签导航
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: '首页',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: '个人资料',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
导航方法
import { useRouter, useLocalSearchParams, Link } from 'expo-router';
function MyComponent() {
const router = useRouter();
const params = useLocalSearchParams();
return (
<>
{/* 声明式导航 */}
<Link href="/profile">前往个人资料</Link>
<Link href={{ pathname: '/user/[id]', params: { id: '123' } }}>
查看用户
</Link>
{/* 命令式导航 */}
<Button onPress={() => router.push('/profile')} title="推送" />
<Button onPress={() => router.replace('/home')} title="替换" />
<Button onPress={() => router.back()} title="返回" />
</>
);
}
动态路由
// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>用户ID: {id}</Text>;
}
受保护的路由
// app/_layout.tsx
import { useAuth } from '@/hooks/useAuth';
import { Redirect, Slot } from 'expo-router';
export default function AppLayout() {
const { user, loading } = useAuth();
if (loading) {
return <LoadingScreen />;
}
if (!user) {
return <Redirect href="/login" />;
}
return <Slot />;
}
状态管理
全局状态的Context API
// contexts/AppContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AppState {
user: User | null;
theme: 'light' | 'dark';
setUser: (user: User | null) => void;
setTheme: (theme: 'light' | 'dark') => void;
}
const AppContext = createContext<AppState | undefined>(undefined);
export function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<AppContext.Provider value={{ user, theme, setUser, setTheme }}>
{children}
</AppContext.Provider>
);
}
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp必须在AppProvider内使用');
}
return context;
};
Redux Toolkit(用于复杂状态)
// store/slices/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
currentUser: User | null;
loading: boolean;
}
const userSlice = createSlice({
name: 'user',
initialState: { currentUser: null, loading: false } as UserState,
reducers: {
setUser: (state, action: PayloadAction<User | null>) => {
state.currentUser = action.payload;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
},
});
export const { setUser, setLoading } = userSlice.actions;
export default userSlice.reducer;
Zustand(轻量级替代方案)
// store/useStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AppStore {
user: User | null;
theme: 'light' | 'dark';
setUser: (user: User | null) => void;
toggleTheme: () => void;
}
export const useStore = create<AppStore>()(
persist(
set => ({
user: null,
theme: 'light',
setUser: user => set({ user }),
toggleTheme: () =>
set(state => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{
name: 'app-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
离线支持
本地数据的AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
// 保存数据
await AsyncStorage.setItem('user', JSON.stringify(user));
// 加载数据
const userData = await AsyncStorage.getItem('user');
const user = userData ? JSON.parse(userData) : null;
// 删除数据
await AsyncStorage.removeItem('user');
// 清除所有
await AsyncStorage.clear();
复杂离线数据的SQLite
import * as SQLite from 'expo-sqlite';
class DatabaseService {
private db: SQLite.SQLiteDatabase | null = null;
async init() {
this.db = await SQLite.openDatabaseAsync('myapp.db');
await this.db.execAsync(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
timestamp INTEGER,
synced INTEGER DEFAULT 0
);
`);
}
async saveMessage(text: string) {
await this.db?.runAsync(
'INSERT INTO messages (text, timestamp) VALUES (?, ?)',
text,
Date.now()
);
}
async getUnsynced() {
return await this.db?.getAllAsync('SELECT * FROM messages WHERE synced = 0');
}
async markSynced(id: number) {
await this.db?.runAsync('UPDATE messages SET synced = 1 WHERE id = ?', id);
}
}
网络状态检测
import NetInfo from '@react-native-community/netinfo';
import { useEffect, useState } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsOnline(state.isConnected ?? false);
});
return () => unsubscribe();
}, []);
return isOnline;
}
// 使用
function MyScreen() {
const isOnline = useOnlineStatus();
return (
<View>
{!isOnline && <Banner message="您已离线" />}
{/* 其余内容 */}
</View>
);
}
离线优先数据同步
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useOfflineData() {
const queryClient = useQueryClient();
const isOnline = useOnlineStatus();
const { data } = useQuery({
queryKey: ['items'],
queryFn: fetchItems,
enabled: isOnline,
// 离线时使用缓存数据
staleTime: Infinity,
});
const mutation = useMutation({
mutationFn: createItem,
onMutate: async newItem => {
// 乐观更新
await queryClient.cancelQueries({ queryKey: ['items'] });
const previous = queryClient.getQueryData(['items']);
queryClient.setQueryData(['items'], (old: any) => [...old, newItem]);
// 离线时保存到本地存储
if (!isOnline) {
await saveToQueue(newItem);
}
return { previous };
},
onError: (err, newItem, context) => {
// 错误时回滚
queryClient.setQueryData(['items'], context?.previous);
},
});
// 在线时同步队列
useEffect(() => {
if (isOnline) {
syncQueue();
}
}, [isOnline]);
return { data, mutation };
}
推送通知
使用Expo Notifications设置
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// 配置通知处理程序
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
async function registerForPushNotifications() {
if (!Device.isDevice) {
alert('推送通知仅在物理设备上有效');
return;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return;
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = await Notifications.getExpoPushTokenAsync({ projectId });
// 将令牌发送到后端
await sendTokenToBackend(token.data);
// Android特定通道设置
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token.data;
}
处理通知
import { useEffect, useRef } from 'react';
function useNotifications() {
const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>();
useEffect(() => {
// 前台通知处理程序
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
console.log('收到通知:', notification);
});
// 用户交互处理程序
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const data = response.notification.request.content.data;
// 基于通知数据导航
if (data.screen) {
router.push(data.screen as any);
}
});
return () => {
if (notificationListener.current) {
Notifications.removeNotificationSubscription(notificationListener.current);
}
if (responseListener.current) {
Notifications.removeNotificationSubscription(responseListener.current);
}
};
}, []);
}
本地通知
async function scheduleNotification() {
await Notifications.scheduleNotificationAsync({
content: {
title: '提醒',
body: '该检查任务了!',
data: { screen: '/tasks' },
},
trigger: {
seconds: 60,
// 或使用特定日期
// date: new Date(Date.now() + 60 * 60 * 1000),
// 或重复
// repeats: true,
},
});
}
// 取消通知
const identifier = await scheduleNotification();
await Notifications.cancelScheduledNotificationAsync(identifier);
// 取消所有
await Notifications.cancelAllScheduledNotificationsAsync();
深度链接
配置深度链接
// app.json
{
"expo": {
"scheme": "myapp",
"ios": {
"associatedDomains": ["applinks:myapp.com"]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "myapp.com"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
在Expo Router中处理深度链接
// Expo Router自动处理深度链接
// myapp://user/123 -> app/user/[id].tsx
// 自定义处理:
import * as Linking from 'expo-linking';
function useDeepLinking() {
useEffect(() => {
// 获取初始URL(通过链接打开应用)
Linking.getInitialURL().then(url => {
if (url) {
handleDeepLink(url);
}
});
// 监听URL变化(应用已打开)
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
}
function handleDeepLink(url: string) {
const { path, queryParams } = Linking.parse(url);
// 基于路径导航
if (path === 'user') {
router.push(`/user/${queryParams?.id}`);
}
}
通用链接(iOS)和应用链接(Android)
// apple-app-site-association(在https://myapp.com/.well-known/提供服务)
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.company.myapp",
"paths": ["/user/*", "/post/*"]
}
]
}
}
// assetlinks.json(在https://myapp.com/.well-known/提供服务)
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.company.myapp",
"sha256_cert_fingerprints": ["指纹"]
}
}
]
从推送通知深度链接
// 从后端发送推送通知时
{
"to": "ExponentPushToken[xxx]",
"title": "新消息",
"body": "您有一条新消息",
"data": {
"url": "myapp://chat/123"
}
}
// 在应用中处理
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const url = response.notification.request.content.data.url;
if (url) {
Linking.openURL(url);
}
});
性能优化
记忆化
import { memo, useMemo, useCallback } from 'react';
const ListItem = memo(({ item, onPress }: Props) => (
<TouchableOpacity onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</TouchableOpacity>
));
function MyList({ items }: Props) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.title.localeCompare(b.title)),
[items]
);
const handlePress = useCallback((id: string) => {
router.push(`/item/${id}`);
}, []);
return (
<FlatList
data={sortedItems}
renderItem={({ item }) => (
<ListItem item={item} onPress={handlePress} />
)}
keyExtractor={(item) => item.id}
/>
);
}
优化列表
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
estimatedItemSize={100}
// 对于大列表比FlatList快得多
/>
图像优化
import { Image } from 'expo-image';
<Image
source={{ uri: 'https://example.com/large-image.jpg' }}
placeholder={require('./placeholder.png')}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
style={{ width: 300, height: 200 }}
/>
</说明>
<示例> 示例使用:
用户: "审查此代码是否符合expo移动应用规则"
代理: [根据指南分析代码并提供具体反馈]
</示例>
内存协议(强制)
开始前:
cat .claude/context/memory/learnings.md
完成后: 记录任何发现的新模式或例外。
假设中断:您的上下文可能重置。如果不在内存中,它就没有发生。