Expo移动应用开发规则Skill expo-mobile-app-rule

此技能用于指导开发者使用Expo框架进行移动应用开发,遵循最佳实践,涵盖导航、状态管理、离线支持、推送通知、深度链接和性能优化等方面。关键词:Expo, 移动开发, React Native, 导航, 状态管理, 离线支持, 推送通知, 深度链接, 性能优化

移动开发 0 次安装 0 次浏览 更新于 3/10/2026

名称: 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

完成后: 记录任何发现的新模式或例外。

假设中断:您的上下文可能重置。如果不在内存中,它就没有发生。