原生模块
Push/Auth/Security。
原生模块与设备能力
这一课你将能做到
- 用 expo-image-picker 实现「从相册选图」和「调相机拍照」,拿到图片并显示或上传
- 用 expo-location 获取用户经纬度,理解 iOS 权限文案配置和权限被拒后的补救流程
- 用 expo-notifications 发送本地定时通知,并处理「权限被拒且不再弹窗」的引导逻辑
为什么要学
调相机、读相册、拿定位、发通知——这些是 App 区别于网页的核心能力,也是几乎所有真实项目都绕不开的功能:换头像要用相机相册、外卖和打车要用定位、提醒类应用离不开通知。好消息是 Expo 把这些原生能力全部封装成了统一的 JS API,你不用写一行 Swift 或 Kotlin。
但真正的难点不在调 API,而在权限。移动操作系统把敏感能力的生杀大权交给了用户:用户不点头,你的代码写得再漂亮也是死的。而且权限流程坑很多——iOS 上系统授权框只弹一次,用户拒绝之后就只能引导他去系统设置里手动开启;权限用途文案写不好,上架审核直接被打回。
人话:调原生能力就像去政务大厅办事——先取号排队(申请权限),窗口工作人员同意了才能办业务(调 API);如果被拒了,你得礼貌地告诉群众去哪里补办手续(用 Linking.openSettings() 引导去系统设置页),而不是反复按铃骚扰。
先建立图像
权限申请的标准流程图,这一课所有能力都遵循它:
要用功能
↓
检查权限状态
├─ 已授权 → 直接用
├─ 未询问 → 弹系统授权框 → 用户同意 → 用
│ ↘ 用户拒绝 → 降级提示
└─ 已拒绝且不可再问(iOS 典型)
→ 弹自己的解释对话框 → 引导去系统设置页手动开启
Expo 权限 API 的统一模样(所有模块都是这个套路):
// 套路 A:Hook 形式(推荐,组件里用)
const [permissionResponse, requestPermission] = ImagePicker.useCameraPermissions();
// permissionResponse.status: 'undetermined' | 'denied' | 'granted'
// permissionResponse.canAskAgain: false 表示 iOS 上系统不再弹框
// 套路 B:命令式函数(任何位置都能调)
const { status, canAskAgain } = await ImagePicker.requestCameraPermissionsAsync();
再看一眼选图完成后的完整数据流,后面每一步都在这张图里:
用户点「选择头像」
→ launchImageLibraryAsync() 打开系统相册
→ 用户选完图,返回 { canceled: false, assets: [{ uri, width, height, ... }] }
→ uri(file:// 开头的本地路径)→ 给 <Image source={{ uri }}> 显示
→ 或塞进 FormData 上传到服务器(第 7 课学过)
跟着做
步骤 1:装库并实现「从相册选头像」
npx expo install expo-image-picker
注意用 npx expo install 而不是 npm install——它会自动匹配与你 Expo SDK 版本兼容的原生库版本,省去版本对不上的崩溃烦恼。
新建 AvatarPicker.js,一个可直接运行的完整组件:
import * as ImagePicker from 'expo-image-picker';
import { useState } from 'react';
import { Alert, Button, Image, StyleSheet, View } from 'react-native';
export default function AvatarPicker() {
const [uri, setUri] = useState(null);
const pick = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'], // 只要图片(新版写法,不用 ImagePicker.MediaTypeOptions)
allowsEditing: true, // 弹出系统裁剪框
aspect: [1, 1], // 裁剪比例:正方形头像
quality: 0.8, // 压缩质量 0~1,原图太大必须压
});
if (result.canceled) return; // 用户点了取消,直接返回
const asset = result.assets[0]; // 新版返回结构:assets 数组
console.log(asset.uri, asset.width, asset.height, asset.fileSize);
setUri(asset.uri);
};
return (
<View style={styles.container}>
{uri ? (
<Image source={{ uri }} style={styles.avatar} />
) : (
<View style={[styles.avatar, styles.placeholder]} />
)}
<Button title="选择头像" onPress={pick} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 16 },
avatar: { width: 120, height: 120, borderRadius: 60 },
placeholder: { backgroundColor: '#ddd' },
});