返回

Expo笔记

目录

常见命令

新建工程

1
2
3
4
npx create-expo-app@latest

//创建babel.config.js
npx expo customize babel.config.js
1
2
3
4
5
6
7
8
9
//启动服务器
npx expo start
//预编译
npx expo prebuild
//本地编译原生安卓应用
npx expo run:android
npx expo run:ios
//安装新的库
npx expo install package-name

错误检查

1
npx expo-doctor

前面的知识

存储数据

Expo SecureStore

1
npx expo install expo-secure-store
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// utils/secureStore.ts
import * as SecureStore from 'expo-secure-store';

// 可选:定义密钥常量,避免魔法字符串
export const SECURE_STORE_KEYS = {
  ACCESS_TOKEN: 'ACCESS_TOKEN',
  REFRESH_TOKEN: 'REFRESH_TOKEN',
  USER_ID: 'USER_ID',
} as const;

/**
 * 安全存储 - 设置值
 * @param key 存储键(建议使用 SECURE_STORE_KEYS 中的常量)
 * @param value 要存储的字符串值
 * @returns Promise<boolean> 是否成功
 */
export const setSecureItem = async (key: string, value: string): Promise<boolean> => {
  try {
    await SecureStore.setItemAsync(key, value, {
      // 可选:指定加密选项(默认已足够安全)
      keychainAccessible: SecureStore.ALWAYS_THIS_DEVICE_ONLY, // iOS: 仅当前设备,不解锁也能访问(根据需求调整)
      keychainService: 'com.yourcompany.yourapp.securestore', // iOS: 自定义服务名(可选)
    });
    return true;
  } catch (error) {
    console.error(`[SecureStore] Failed to set item "${key}":`, error);
    return false;
  }
};

/**
 * 安全存储 - 获取值
 * @param key 存储键
 * @returns Promise<string | null> 成功返回字符串,失败或不存在返回 null
 */
export const getSecureItem = async (key: string): Promise<string | null> => {
  try {
    const value = await SecureStore.getItemAsync(key);
    return value; // 若不存在,返回 null
  } catch (error) {
    console.error(`[SecureStore] Failed to get item "${key}":`, error);
    return null;
  }
};

/**
 * 安全存储 - 删除值
 * @param key 存储键
 * @returns Promise<boolean> 是否成功
 */
export const deleteSecureItem = async (key: string): Promise<boolean> => {
  try {
    await SecureStore.deleteItemAsync(key);
    return true;
  } catch (error) {
    console.error(`[SecureStore] Failed to delete item "${key}":`, error);
    return false;
  }
};

/**
 * 安全存储 - 清除所有(谨慎使用!仅用于登出等场景)
 * 注意:只会清除当前应用写入的项
 */
export const clearSecureStore = async (): Promise<boolean> => {
  try {
    // Expo SecureStore 没有直接提供 clearAll,需逐个删除
    // 建议:维护一个你使用的 key 列表
    const keys = Object.values(SECURE_STORE_KEYS);
    await Promise.all(keys.map(key => SecureStore.deleteItemAsync(key)));
    return true;
  } catch (error) {
    console.error('[SecureStore] Failed to clear all items:', error);
    return false;
  }
};

Expo SQLite

Expo FileSystem

1
npx expo install expo-file-system
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as DocumentPicker from 'expo-document-picker';

const pickAndSlice = async () => {
  // 1. 用户选择文件
  const result = await DocumentPicker.getDocumentAsync({
    type: '*/*',
  });

  if (result.canceled) return;

  const file = result.assets[0];

  // 2. 分片:取前 512 字节,并显式指定类型
  const headerChunk = file.slice(0, 512, 'application/octet-stream');

  // 3. 读取分片内容(例如检查文件头)
  const reader = new FileReader();
  reader.onload = (e) => {
    const arrayBuffer = e.target?.result as ArrayBuffer;
    const view = new Uint8Array(arrayBuffer);
    console.log('File header bytes:', view.slice(0, 10)); // 打印前10字节
  };
  reader.readAsArrayBuffer(headerChunk);
};

页面导航(Expo Router)

1
2
3
4
5
6
7
8
app
    -(root_group_name)
        index.tsx
        [productId].tsx
        _layout.tsx
    - child_page
        index.tsx
        [params].tsx

pre

1
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar

导航

useRouter

1
2
3
4
5
6
7
8
9
// router.push  router.back  router.replace
import { useRouter } from 'expo-router';
import { Button } from 'react-native';

export default function Home() {
  const router = useRouter();

  return <Button title="Go to About" onPress={() => router.navigate('/about')} />;
}
1
2
3
4
5
6
7
8
9
<Link href="/about">About</Link>
// 使用相对地址
<Link href="./article">Go to article</Link>
//使用asChild 来声明Link包裹了子元素
<Link href="/other" asChild>
  <Pressable>
    <Text>Home</Text>
  </Pressable>
</Link>

使用动态路由

传递参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<Link href="/users?limit=20">View users</Link>

<View>
  <Link
    href="/user/bacon">
    View user (id inline)
  </Link>
  <Link
    href={{
      pathname: '/user/[id]',
      params: { id: 'bacon' }
    }}
  >
    View user (id in params in href)
  </Link>
  <Pressable
    onPress={() =>
      router.navigate({
        pathname: '/user/[id]',
        params: { id: 'bacon' }
      })
    }
  >
    <Text>View user (imperative)</Text>
  </Pressable>
</View>

// 在不重新进入页面的情况下更新页面参数
<Pressable onPress={() => router.setParams({ limit: 50 })}>
  <Text>View more users</Text>
</Pressable>

接收参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function Users() {
  const { id, limit } = useLocalSearchParams();

  return (
    <View>
      <Text>User ID: {id}</Text>
      <Text>Limit: {limit}</Text>
    </View>
  );
}

Stack

手动编写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: '首页' }} />
      <Stack.Screen name="profile" options={{ title: '个人资料' }} />
      <Stack.Screen
        name="settings"
        options={{
          title: '设置',
          headerStyle: { backgroundColor: '#f0f0f0' },
          headerTintColor: '#000',
        }}
      />
      <Stack.Screen name="[productId]" options={{ headerShown: false }} />
    </Stack>
  );
} 

自动

1
<Stack screenOptions={{ headerShown: false }} />

Tabs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { MaterialIcons } from '@expo/vector-icons'; // 官方推荐图标库

export default function TabsLayout() {
  return (
    <Tabs
      screenOptions={{
        // --- 全局 Tab 样式 ---
        tabBarActiveTintColor: '#1e90ff',       // 选中时颜色
        tabBarInactiveTintColor: '#888',        // 未选中颜色
        tabBarStyle: {
          height: 60,
          paddingBottom: 6,
          paddingTop: 6,
          borderTopWidth: 1,
          borderTopColor: '#eee',
        },

        // --- 每个页面的 Header 控制(统一设置)---
        headerShown: true,                      // 默认显示 header
        headerStyle: {
          backgroundColor: '#fff',
        },
        headerShadowVisible: false,             // 去掉 header 底部阴影(iOS/Android 统一)
        headerTitleAlign: 'center',             // 标题居中
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: '首页',
          headerShown: false, // 首页通常不需要 header
          tabBarIcon: ({ color, size }) => (
            <MaterialIcons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: '我的',
          headerTitle: '个人中心', // 自定义 header 标题(不同于 tab 标题)
          tabBarIcon: ({ color, size }) => (
            <MaterialIcons name="person" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: '设置',
          tabBarIcon: ({ color, size }) => (
            <MaterialIcons name="settings" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Drawer

Slots

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 访问/ 则slot渲染index.tsx的内容
// 访问/profile 则slot渲染profile.tsx的内容
// app/_layout.tsx
import { View, Text } from 'react-native';
import { Slot } from 'expo-router';

export default function Layout() {
  return (
    <View style={{ flex: 1 }}>
      {/* 全局 Header */}
      <Text style={{ fontSize: 20, textAlign: 'center', padding: 10 }}>
        MyApp
      </Text>

      {/* 子页面内容在这里显示 */}
      <Slot />
    </View>
  );
}

路由守卫

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { Stack } from 'expo-router';
import { useAuthState } from '@/utils/authState';

export default function RootLayout() {
  const { isLoggedIn } = useAuthState();

  return (
    <Stack>
      <Stack.Protected guard={isLoggedIn}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="modal" />
      </Stack.Protected>

      <Stack.Protected guard={!isLoggedIn}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="create-account" />
      </Stack.Protected>
    </Stack>
  );
}

expo file

GlueStack(UI库)

Licensed under CC BY-NC-SA 4.0