CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/原生模块
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

原生模块

Push/Auth/Security。

30 分钟

原生模块与设备能力

这一课你将能做到

  1. 用 expo-image-picker 实现「从相册选图」和「调相机拍照」,拿到图片并显示或上传
  2. 用 expo-location 获取用户经纬度,理解 iOS 权限文案配置和权限被拒后的补救流程
  3. 用 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' },
});

跑起来,点按钮,系统相册弹出,选一张图,圆形头像立刻显示。整条链路一次通过的感觉非常爽。

步骤 2:调相机拍照(要先申请权限)

相机和相册不同:相册在新系统上基本免权限(用户手动选哪张给你哪张),相机则必须显式申请。给上面的组件加一个拍照按钮:

const takePhoto = async () => {
  // 第一步:申请权限
  const { status, canAskAgain } = await ImagePicker.requestCameraPermissionsAsync();

  if (status !== 'granted') {
    if (!canAskAgain) {
      // iOS 上被拒过一次,系统不再弹框,只能引导去设置
      Alert.alert('需要相机权限', '请在系统设置中允许访问相机', [
        { text: '取消', style: 'cancel' },
        { text: '去设置', onPress: () => Linking.openSettings() },
      ]);
    } else {
      Alert.alert('提示', '没有相机权限,无法拍照');
    }
    return;
  }

  // 第二步:权限到手,调起相机
  const result = await ImagePicker.launchCameraAsync({
    quality: 0.8,
    allowsEditing: true,
  });
  if (!result.canceled) {
    setUri(result.assets[0].uri);
  }
};

文件顶部记得补 import:

import { Linking } from 'react-native';

重要提醒:iOS 模拟器没有相机硬件,调起来是黑屏或直接报错。相机功能请用真机调试(Expo Go 扫码即可,第 1 课的环境)。

步骤 3:获取定位(含 iOS 权限文案配置)

npx expo install expo-location
import * as Location from 'expo-location';

const getLocation = async () => {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') {
    Alert.alert('提示', '没有定位权限,无法推荐附近内容');
    return;
  }

  const loc = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced, // 精度与耗电的折中
  });
  console.log('纬度', loc.coords.latitude, '经度', loc.coords.longitude);
};

拿到经纬度后,通常配合地图组件(react-native-maps)展示,或发给后端换「附近的店」列表。

关键配置:iOS 上架时必须在 app.json 里声明权限用途文案,否则审核必拒。这段话会在系统授权框里展示给用户:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSLocationWhenInUseUsageDescription": "需要获取您的位置,用于推荐附近的内容和服务"
        }
    }
  }
}

文案原则:说清楚「要这个权限干什么、给用户带来什么好处」。写「我们需要定位信息」这种空洞的话,苹果审核员会直接打回。

步骤 4:发一条本地通知

npx expo install expo-notifications
import * as Notifications from 'expo-notifications';

// App 启动时设置一次:通知在前台也要弹出来(默认前台静默)
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

const remindMe = async () => {
  const { granted } = await Notifications.requestPermissionsAsync();
  if (!granted) {
    Alert.alert('提示', '没有通知权限,收不到提醒');
    return;
  }

  await Notifications.scheduleNotificationAsync({
    content: {
      title: '喝水提醒 💧',
      body: '该起来活动一下了',
      sound: true,
    },
    trigger: { type: 'timeInterval', seconds: 5 }, // 5 秒后触发
  });

  Alert.alert('已设置', '5 秒后会收到提醒,可以切到桌面等');
};

点按钮后立刻切到手机桌面,5 秒后通知横幅弹出——这是「代办提醒」「喝水打卡」类功能的雏形。注意 Android 13(API 33)起通知也需要运行时权限了,上面的 requestPermissionsAsync 会一并处理。

步骤 5:统一的权限拒绝引导组件

每个权限都写一遍「去设置」逻辑太啰嗦,抽一个可复用函数:

import { Alert, Linking } from 'react-native';

export function alertPermissionDenied(permissionName) {
  Alert.alert(
    `需要${permissionName}权限`,
    `该功能需要您授权${permissionName}。请前往系统设置开启。`,
    [
      { text: '取消', style: 'cancel' },
      { text: '去设置', onPress: () => Linking.openSettings() },
    ]
  );
}

Linking.openSettings() 会直接跳到本 App 在系统设置里的专属页面,用户拨一下开关就行——这是权限被拒后唯一有效的补救路径。

讲清楚要点

  1. 权限永远「先申请、再使用」:不申请直接调 API,轻则静默失败(返回空),重则直接崩溃红屏。Expo 每个模块都提供 request...PermissionsAsync() 或对应的 use...Permissions() Hook,名字规律统一,看到模块名就能猜出来。
  2. iOS 权限文案是上架硬性要求:app.json 的 ios.infoPlist 里要为每个用到的敏感权限写用途说明(相机、定位、相册、麦克风……),文案要让用户秒懂「为什么给你」,空洞文案会被审核拒绝。
  3. ImagePicker 新版返回结构是 { canceled, assets }:图片信息在 result.assets[0] 里,注意 canceled 是单 l 的美式拼写。网上老教程里的 result.cancelled(双 l)和 result.uri 都是旧版 API,照抄会拿到 undefined。
  4. 拿到的 uri 是临时路径,重启后可能失效:系统会清理缓存目录。需要长期保存的图片,必须用 expo-file-system 把文件拷贝到 App 的文档目录(FileSystem.documentDirectory),再存新路径。
  5. 本地通知 vs 远程推送是两套东西:本地通知完全在设备上调度,不联网、不需要服务端,提醒类场景足够用;远程推送要走「设备 token → Expo Push Service / APNs / FCM → 你的服务端」整条链路,是另一个量级的工程。
  6. 模拟器有硬边界:iOS 模拟器没有相机;定位在模拟器上可以模拟(iOS 模拟器菜单 Features → Location,安卓模拟器在设置里),但指南针、陀螺仪这类传感器都没有。设备能力相关功能,真机调试是唯一可靠方式。

你可能会懵的地方

  • 为什么安卓要的权限比 iOS 多还碎? Android 把权限切得更细:定位分精确/模糊,相册分读图/读视频,通知从 13 开始也要运行时权限。好消息是用 Expo 时这些声明大多由库的默认配置帮你写进 AndroidManifest,你只需要在运行时调 requestPermissionsAsync 申请。别手动往 app.json 里乱加权限,多声明了没用的权限反而会被 Google Play 要求解释。

  • 能不能先弹一个自己设计的界面解释用途,再弹系统框? 不但允许,而且是最佳实践,行话叫「预授权弹窗(pre-permission prompt)」。在用户第一次进入相关功能时,先用自己的 UI 说明「开启定位可以帮你找到附近的朋友」,用户点了「开启」再调系统的 request——这样即使被拒,下次进功能还能再弹自己的界面引导,转化率显著高于冷不丁弹系统框。

  • 选完图想立刻上传到服务器,怎么做? 用第 7 课学过的 FormData:formData.append('file', { uri, name: 'avatar.jpg', type: 'image/jpeg' }),然后 fetch(url, { method: 'POST', body: formData })。注意这里传的是包含 uri/name/type 三字段的对象,不是 Blob——这是 RN 的特有写法,后端收到的就是标准 multipart 文件。

  • 通知被点击后,怎么跳到 App 里的指定页面? 用 Notifications.addNotificationResponseReceivedListener(response => ...) 监听点击事件,从 response.notification.request.content.data 里取你调度时塞的自定义数据(比如 { screen: 'OrderDetail', id: 123 }),再调第 5 课的 navigationRef 跳转。

  • ** Expo 没有的冷门能力(比如蓝牙打印机)怎么办?** 三条路:找社区封装好的库;用 Expo Modules API 自己写 Swift/Kotlin 封装(比想象中简单);npx expo prebuild 生成原生工程后按纯 RN 方式集成。初学者遇到这种需求,先确认 Expo 生态里真的没有再说。

错法对照

错法问题正解
不申请权限直接 launchCameraAsync崩溃或静默失败先 request 再 launch
用旧版 result.cancelled、result.uriundefined,逻辑全失效新版:result.canceled + result.assets[0].uri
权限被拒后反复调 requestiOS 只弹一次,之后调用无效检查 canAskAgain,false 就引导去设置页
忘记配 iOS 权限用途文案上架审核被拒app.json 的 ios.infoPlist 写清用途
在 iOS 模拟器上调试相机黑屏/报错换真机(Expo Go 扫码)
持久化时只存临时 uri几天后图片打不开用 expo-file-system 拷到文档目录再存路径
Android 13+ 不发通知权限申请通知永远不弹requestPermissionsAsync 统一处理
权限文案写「我们需要XX权限」审核打回写用户视角的收益:「用于推荐附近内容」

卡点急救

  1. 改了 app.json 权限配置不生效:配置进的是原生包,Expo Go 内置了常用权限所以开发时看不出差别;正式发布前用 EAS Build 重新打包才生效。自定义权限文案同理。
  2. Android 真机定位拿不到:依次检查——手机系统定位总开关是否打开、App 权限是否给了、是否在室内 GPS 搜不到星(走到窗边试试)。
  3. 通知设了时间不弹:检查系统设置里本 App 的通知总开关;确认 trigger 的 seconds 大于 0;前台默认不弹横幅,记得调 setNotificationHandler。
  4. 选图后 Image 不显示:先 console.log(uri) 确认非空;source={{ uri }} 注意是对象包一层;样式里必须给宽高,RN 的 Image 不给尺寸不渲染。
  5. launchImageLibraryAsync 闪退:多见于 npm 直装版本不匹配,改用 npx expo install expo-image-picker 重装对齐版本。

面试常问

  • RN 是如何调用原生能力的? JS 代码通过 JSI(旧架构是异步 Bridge)调用原生侧注册的模块方法,原生执行完把结果回调给 JS。Expo 把相机、定位、通知等常用能力用 Expo Modules API 预封装成 JS 接口;遇到没封装的,开发者可以用同一套 API 自己写原生模块,JS 侧无感知。

  • iOS 上用户拒绝了权限,产品要求「下次进页面再问问」,怎么做? 系统授权框只弹一次,这是硬限制。能做的:检测到 status 为 denied 且 canAskAgain 为 false 时,弹自己的解释对话框,用户确认后用 Linking.openSettings() 跳到本 App 的系统设置页引导手动开启。进页面时可以再调 getPermissionsAsync(不弹框、只查询)检查状态,若已开启则恢复正常流程。

  • 本地通知和远程推送的区别与适用场景? 本地通知由设备本地调度,不需要网络和服务端,适合闹钟、打卡提醒这类时间确定的场景。远程推送由服务端经 APNs(iOS)/ FCM(Android)下发,适合消息、订单状态变更这类由服务端事件触发的场景,需要设备 token 上报、证书配置和服务端集成,复杂度高一个量级。

自测三问

  1. 调用相机的完整流程是什么?(提示:权限申请 → 判断 status → 处理 canAskAgain → launch)
  2. ImagePicker 选图成功后,图片 uri 从哪个字段拿?为什么这个 uri 不能直接长期存储?
  3. iOS 用户拒绝了定位权限且系统不再弹框,你的补救代码该怎么写?

小结一句话

原生能力的核心套路就两步:先申请权限(被拒要优雅引导去设置页),再调 Expo 封装好的模块 API——相机、定位、通知全是这个模子,掌握一个等于掌握全部。

延伸

  • Expo ImagePicker 文档
  • Expo Location 文档
  • Expo Notifications 文档
  • 下一课:动画——用 Animated 和 Reanimated 让界面活起来,告别 PPT 式生硬切换。
加载本节练习…
← 上一课本地存储下一课 →动画