移动/游戏
使用 React Native 和 Expo 构建跨平台…
需要以一套代码构建 iOS 和 Android 移动应用并使用 React Native/Expo 时。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
使用 React Native 和 Ex特定
解决思路
- 创建新 Expo 应用:运行 `npx create-expo-app my-app --template blank-typescript`,然后 `cd my-app`。
- 安装依赖:运行 `npx expo install react-native-screens react-native-safe-area-context`,再运行 `npx expo install expo-router`。
- 启动开发:运行 `npx expo start`。
- 组织项目结构:在 `app/` 下放置 `(tabs)/index.tsx`、`(tabs)/profile.tsx`、`(tabs)/_layout.tsx`、`users/[id].tsx`、`_layout.tsx`、`+not-found.tsx`;在 `components/` 下放置 `Button.tsx`、`Card.tsx`、`Loading.tsx`;在 `hooks/` 下放置 `useAuth.ts`;保留 `app.json` 和 `package.json`。
- 实现 Tab 导航:在 `app/(tabs)/_layout.tsx` 中从 `expo-router` 引入 `Tabs`,从 `@expo/vector-icons` 引入 `Ionicons`;设置 `screenOptions` 的 `tabBarActiveTintColor: '#007AFF'` 和 `headerShown: false`;添加 `Tabs.Screen name='index'` 并配置 `title: 'Home'`、`tabBarIcon` 返回 `Ionicons name='home'`;添加 `Tabs.Screen name='profile'` 并配置 `title: 'Profile'`、`tabBarIcon` 返回 `Ionicons name='person'`。
- 实现 Stack 导航:在 `app/users/[id].tsx` 中使用 `useLocalSearchParams` 获取 `id`,并用 `View` 和 `Text` 渲染该 `id`。
- 创建自定义 Button:在 `components/Button.tsx` 中使用 `TouchableOpacity`、`Text`、`StyleSheet`、`ActivityIndicator`;接收 `title`、`onPress`、`variant`(`'primary' | 'secondary'`)、`loading`、`disabled` 属性;根据 `variant` 应用 `primary` 或 `secondary` 样式,`disabled` 时应用 `disabled` 样式;`onPress` 绑定点击,`disabled` 或 `loading` 时禁用;`loading` 为真时显示 `ActivityIndicator`,否则显示 `Text`;定义 `button`、`primary`、`secondary`、`disabled`、`text` 样式。
- 创建 Card 组件:在 `components/Card.tsx` 中使用 `View`、`Text`、`StyleSheet`、`TouchableOpacity` 和 `ReactNode`;接收 `title`、`children`、`onPress`;当有 `onPress` 时用 `TouchableOpacity` 作为容器,否则用 `View`;应用 `card` 样式(白色背景、圆角 12、内边距 16、阴影、`elevation: 3`);有 `title` 时渲染 `Text`;定义 `title` 样式。
- 创建数据获取 Hook `useQuery`:在 `hooks/useQuery.ts` 中使用 `useState` 和 `useEffect`;维护 `data`、`loading`、`error`、`refetch`;`fetchData` 中 `fetch(url)`,检查 `response.ok`,解析 `json` 并 `setData`,捕获异常并 `setError`,最后 `setLoading(false)`;在 `useEffect` 中依赖 `url` 调用 `fetchData`;返回这些状态和方法。
- 使用 `useQuery`:在 `app/(tabs)/index.tsx` 中调用 `useQuery<Post[]>('https://api.example.com/posts')`;有错误时渲染错误信息;用 `FlatList` 渲染数据,`keyExtractor` 返回 `item.id`,`renderItem` 用 `Card` 显示 `item.title` 和 `item.content`;用 `RefreshControl` 绑定 `loading` 和 `refetch`。
- 集成相机:在 `app/camera.tsx` 中使用 `expo-camera` 的 `Camera` 和 `CameraType`;用 `useState` 管理 `type` 初始为 `CameraType.back`;用 `Camera.useCameraPermissions()` 获取权限;权限未决定时返回空 `View`;未授权时显示请求权限按钮;授权后渲染 `Camera`,并用按钮在 `CameraType.back` 和 `CameraType.front` 之间切换;定义 `container`、`camera`、`buttonContainer` 样式。
- 集成推送通知:在 `hooks/useNotifications.ts` 中使用 `expo-notifications`、`expo-device`、`Platform`;调用 `Notifications.setNotificationHandler` 配置 `shouldShowAlert`、`shouldPlaySound`、`shouldSetBadge`;在 `useNotifications` 中维护 `expoPushToken` 状态,调用 `registerForPushNotificationsAsync` 获取 token;添加 `addNotificationReceivedListener` 和 `addNotificationResponseReceivedListener`,并在清理时移除订阅;在 `registerForPushNotificationsAsync` 中,Android 平台设置通知 channel `default`(`importance: MAX`、`vibrationPattern: [0, 250, 250, 250]`、`lightColor: '#FF231F7C'`);检查 `Device.isDevice`,获取并请求权限,未授权时 `alert`,最后用 `Notifications.getExpoPushTokenAsync()` 获取 token;非真机时 `alert` 提示必须使用物理设备。
- 集成定位:在 `hooks/useLocation.ts` 中使用 `expo-location`;用 `useState` 管理 `location` 和 `error`;在 `useEffect` 中调用 `Location.requestForegroundPermissionsAsync()` 请求前台权限;未授权时设置错误 `'Permission to access location was denied'`;授权后用 `Location.getCurrentPositionAsync({})` 获取位置并 `setLocation`;返回 `location` 和 `error`。
- 使用 Zustand 管理状态:在 `store/auth.ts` 中用 `create` 创建 `useAuthStore`;状态包含 `user`、`token`;`login` 方法向 `'https://api.example.com/auth/login'` 发送 POST 请求(`Content-Type: application/json`,body 为 `{ email, password }`),解析响应后 `set({ user, token })`;`logout` 方法 `set({ user: null, token: null })`。
- 使用 Auth Store:在 `app/login.tsx` 中用 `useState` 管理 `email` 和 `password`;用 `useAuthStore(state => state.login)` 获取 `login`;用 `TextInput` 绑定输入(`autoCapitalize='none'`、`secureTextEntry`);用 `Button` 触发 `login(email, password)`。
- 优化图片:在 `components/OptimizedImage.tsx` 中使用 `expo-image` 的 `Image`;接收 `uri`、`width`、`height`;设置 `contentFit='cover'`、`transition={200}`、`cachePolicy='memory-disk'`、`placeholder={require('@/assets/placeholder.png')}`。
- 懒加载组件:在 `app/(tabs)/index.tsx` 中使用 `React.lazy` 懒加载 `HeavyComponent`;用 `Suspense` 包裹,`fallback` 为 `ActivityIndicator`。
- 优化列表:使用 `@shopify/flash-list` 的 `FlashList`;传入 `data`、`renderItem`、`estimatedItemSize={100}`、`keyExtractor`。
- 配置 `app.json`:设置 `expo.name`、`slug`、`version`、`orientation`、`icon`、`userInterfaceStyle`、`splash`(`image`、`resizeMode`、`backgroundColor`);设置 `ios.supportsTablet`、`ios.bundleIdentifier`、`ios.buildNumber`;设置 `android.adaptiveIcon`、`android.package`、`android.versionCode`、`android.permissions`(`CAMERA`、`ACCESS_FINE_LOCATION`、`NOTIFICATIONS`);配置 `plugins` 包括 `expo-router`、`expo-camera`(`cameraPermission`)、`expo-location`(`locationAlwaysAndWhenInUsePermission`)。
- 构建 iOS:运行 `npm install -g eas-cli`,运行 `eas login`,运行 `eas build:configure`,运行 `eas build --platform ios`,运行 `eas submit --platform ios`。
- 构建 Android:运行 `eas build --platform android`,运行 `eas submit --platform android`。
- 发布 OTA 更新:运行 `eas update --branch production --message 'Bug fixes'`,用户自动获取更新,无需应用商店审核。
- 测试 Button:使用 Jest + React Native Testing Library;在 `__tests__/Button.test.tsx` 中 `render` 和 `fireEvent`;测试按下时 `onPress` 被调用一次;测试 `loading` 时 `getByTestId('loading-indicator')` 存在。
- 实现受保护路由:在 `app/_layout.tsx` 中使用 `useRouter`、`Slot`、`useAuthStore`;在 `useEffect` 中检查 `user`,无 `user` 时 `router.replace('/login')`;返回 `Slot`。
- 实现表单处理:在 `hooks/useForm.ts` 中使用 `useState` 管理 `values` 和 `errors`;`handleChange` 更新对应字段并清除该字段错误;`validate` 接收校验规则,遍历规则执行校验,设置 `errors`,返回是否无错误;返回 `values`、`errors`、`handleChange`、`validate`。
适用边界 · 注意事项
- 不适用于纯原生 iOS(Swift/Objective-C)或 Android(Kotlin/Java)开发。
- 不适用于不想使用 Expo/EAS 或需要大量自定义原生模块的项目。
- 不适用于 Web 或桌面应用开发。