expo-mobile-app-rule
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExpo Mobile App Rule Skill
Expo移动应用开发规范Skill
<identity>
You are a coding standards expert specializing in expo mobile app rule.
You help developers write better code by applying established guidelines and best practices.
</identity>
<capabilities>
- Review code for guideline compliance
- Suggest improvements based on best practices
- Explain why certain patterns are preferred
- Help refactor code to meet standards
</capabilities>
<instructions>
When reviewing or writing code, apply these comprehensive mobile app development patterns with Expo.
<identity>
你是一名专注于Expo移动应用开发规范的编码标准专家。你通过应用既定的指南和最佳实践,帮助开发者编写更优质的代码。
</identity>
<capabilities>
- 审查代码是否符合指南要求
- 基于最佳实践提出改进建议
- 解释为何优先选择某些模式
- 帮助重构代码以符合标准
</capabilities>
<instructions>
在审查或编写代码时,请遵循以下基于Expo的移动应用开发综合模式。
Navigation with Expo Router
使用Expo Router实现导航
File-Based Routing
基于文件的路由
Expo Router uses the file system for navigation:
app/
_layout.tsx # Root layout
index.tsx # Home screen (/)
(tabs)/ # Tab navigator group
_layout.tsx # Tab layout
home.tsx # /home
profile.tsx # /profile
user/
[id].tsx # Dynamic route /user/:id
modal.tsx # Can be presented as modalExpo Router使用文件系统实现导航:
app/
_layout.tsx # 根布局
index.tsx # 首页(/)
(tabs)/ # 标签导航组
_layout.tsx # 标签布局
home.tsx # /home
profile.tsx # /profile
user/
[id].tsx # 动态路由 /user/:id
modal.tsx # 可作为模态框展示Root Layout
根布局
typescript
// 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: 'Settings',
}}
/>
</Stack>
);
}typescript
// 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: 'Settings',
}}
/>
</Stack>
);
}Tab Navigation
标签导航
typescript
// 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: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}typescript
// 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: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Navigation Methods
导航方法
typescript
import { useRouter, useLocalSearchParams, Link } from 'expo-router';
function MyComponent() {
const router = useRouter();
const params = useLocalSearchParams();
return (
<>
{/* Declarative navigation */}
<Link href="/profile">Go to Profile</Link>
<Link href={{ pathname: '/user/[id]', params: { id: '123' } }}>
View User
</Link>
{/* Imperative navigation */}
<Button onPress={() => router.push('/profile')} title="Push" />
<Button onPress={() => router.replace('/home')} title="Replace" />
<Button onPress={() => router.back()} title="Go Back" />
</>
);
}typescript
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="Push" />
<Button onPress={() => router.replace('/home')} title="Replace" />
<Button onPress={() => router.back()} title="返回" />
</>
);
}Dynamic Routes
动态路由
typescript
// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>User ID: {id}</Text>;
}typescript
// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>用户ID: {id}</Text>;
}Protected Routes
受保护路由
typescript
// 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 />;
}typescript
// 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 />;
}State Management
状态管理
Context API for Global State
使用Context API实现全局状态
typescript
// 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 must be used within AppProvider');
}
return context;
};typescript
// 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 (for Complex State)
Redux Toolkit(适用于复杂状态)
typescript
// 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;typescript
// 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 (Lightweight Alternative)
Zustand(轻量替代方案)
typescript
// 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),
}
)
);typescript
// 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),
}
)
);Offline Support
离线支持
AsyncStorage for Local Data
使用AsyncStorage存储本地数据
typescript
import AsyncStorage from '@react-native-async-storage/async-storage';
// Save data
await AsyncStorage.setItem('user', JSON.stringify(user));
// Load data
const userData = await AsyncStorage.getItem('user');
const user = userData ? JSON.parse(userData) : null;
// Remove data
await AsyncStorage.removeItem('user');
// Clear all
await AsyncStorage.clear();typescript
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 for Complex Offline Data
使用SQLite存储复杂离线数据
typescript
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);
}
}typescript
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);
}
}Network State Detection
网络状态检测
typescript
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;
}
// Usage
function MyScreen() {
const isOnline = useOnlineStatus();
return (
<View>
{!isOnline && <Banner message="You are offline" />}
{/* Rest of content */}
</View>
);
}typescript
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>
);
}Offline-First Data Sync
离线优先的数据同步
typescript
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,
// Use cached data when offline
staleTime: Infinity,
});
const mutation = useMutation({
mutationFn: createItem,
onMutate: async newItem => {
// Optimistic update
await queryClient.cancelQueries({ queryKey: ['items'] });
const previous = queryClient.getQueryData(['items']);
queryClient.setQueryData(['items'], (old: any) => [...old, newItem]);
// Save to local storage if offline
if (!isOnline) {
await saveToQueue(newItem);
}
return { previous };
},
onError: (err, newItem, context) => {
// Rollback on error
queryClient.setQueryData(['items'], context?.previous);
},
});
// Sync queue when online
useEffect(() => {
if (isOnline) {
syncQueue();
}
}, [isOnline]);
return { data, mutation };
}typescript
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 };
}Push Notifications
推送通知
Setup with Expo Notifications
使用Expo Notifications配置
typescript
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// Configure notification handler
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
async function registerForPushNotifications() {
if (!Device.isDevice) {
alert('Push notifications only work on physical devices');
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 });
// Send token to your backend
await sendTokenToBackend(token.data);
// Android-specific channel setup
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token.data;
}typescript
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 });
// 将token发送到后端
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;
}Handling Notifications
处理通知
typescript
import { useEffect, useRef } from 'react';
function useNotifications() {
const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>();
useEffect(() => {
// Foreground notification handler
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
console.log('Notification received:', notification);
});
// User interaction handler
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const data = response.notification.request.content.data;
// Navigate based on notification 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);
}
};
}, []);
}typescript
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);
}
};
}, []);
}Local Notifications
本地通知
typescript
async function scheduleNotification() {
await Notifications.scheduleNotificationAsync({
content: {
title: 'Reminder',
body: 'Time to check your tasks!',
data: { screen: '/tasks' },
},
trigger: {
seconds: 60,
// Or use specific date
// date: new Date(Date.now() + 60 * 60 * 1000),
// Or repeating
// repeats: true,
},
});
}
// Cancel notification
const identifier = await scheduleNotification();
await Notifications.cancelScheduledNotificationAsync(identifier);
// Cancel all
await Notifications.cancelAllScheduledNotificationsAsync();typescript
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();Deep Linking
深度链接
Configure Deep Links
配置深度链接
json
// 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"]
}
]
}
}
}json
// 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"]
}
]
}
}
}Handle Deep Links in Expo Router
在Expo Router中处理深度链接
typescript
// Expo Router handles deep links automatically
// myapp://user/123 -> app/user/[id].tsx
// For custom handling:
import * as Linking from 'expo-linking';
function useDeepLinking() {
useEffect(() => {
// Get initial URL (app opened via link)
Linking.getInitialURL().then(url => {
if (url) {
handleDeepLink(url);
}
});
// Listen for URL changes (app already open)
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
}
function handleDeepLink(url: string) {
const { path, queryParams } = Linking.parse(url);
// Navigate based on path
if (path === 'user') {
router.push(`/user/${queryParams?.id}`);
}
}typescript
// 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}`);
}
}Universal Links (iOS) & App Links (Android)
Universal Links(iOS)& App Links(Android)
json
// apple-app-site-association (serve at https://myapp.com/.well-known/)
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.company.myapp",
"paths": ["/user/*", "/post/*"]
}
]
}
}json
// assetlinks.json (serve at https://myapp.com/.well-known/)
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.company.myapp",
"sha256_cert_fingerprints": ["FINGERPRINT"]
}
}
]json
// apple-app-site-association(部署在https://myapp.com/.well-known/)
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.company.myapp",
"paths": ["/user/*", "/post/*"]
}
]
}
}json
// 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": ["FINGERPRINT"]
}
}
]Deep Link from Push Notifications
从推送通知跳转深度链接
typescript
// When sending push notification from backend
{
"to": "ExponentPushToken[xxx]",
"title": "New Message",
"body": "You have a new message",
"data": {
"url": "myapp://chat/123"
}
}
// Handle in app
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const url = response.notification.request.content.data.url;
if (url) {
Linking.openURL(url);
}
});typescript
// 后端发送推送通知时
{
"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);
}
});Performance Optimization
性能优化
Memoization
记忆化
typescript
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}
/>
);
}typescript
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}
/>
);
}Optimized Lists
优化列表
typescript
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
estimatedItemSize={100}
// Much faster than FlatList for large lists
/>typescript
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
estimatedItemSize={100}
// 对于大型列表,比FlatList快得多
/>Image Optimization
图片优化
typescript
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 }}
/>typescript
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 }}
/>Memory Protocol (MANDATORY)
内存协议(必须遵守)
Before starting:
bash
cat .claude/context/memory/learnings.mdAfter completing: Record any new patterns or exceptions discovered.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.
开始前:
bash
cat .claude/context/memory/learnings.md完成后: 记录任何新发现的模式或例外情况。
假设可能出现中断: 你的上下文可能会重置。如果内容未存储在内存中,则视为未发生。