首页 / 文章

React Native通知:权限、频道与FCM生命周期

了解在结合Notifee的React Native通知系统中,权限、Android频道、FCM令牌以及前台、后台和退出状态处理程序是如何相互配合的。

发布者:REACTAPP.TOP阅读约 19 分钟4332 词

来源: React Native Notifications & Permissions: A Complete Guide for Android and iOS

对用户而言,通知就是一行文字,比如“您的订单已发货”。但对发送通知的应用程序来说,这一行文字的生成依赖于运行时权限、Android通知渠道、设备令牌、Firebase Cloud Messaging(FCM)、后台处理程序,以及Android与iOS之间的各种平台差异。只要其中任何一层出错,就可能导致通知无法送达或打开错误的界面。本指南将结合Notifee和React Native Firebase逐一讲解这些层面,帮助您构建在各种应用状态下都能稳定运行的通知系统。

本地通知与推送通知

本地通知

本地通知是由应用程序在设备上直接创建的,无需服务器参与。会议提醒就是典型的例子:

Reminder
Your meeting starts in 10 minutes.

该应用可以离线显示或安排推送通知。Notifee通过两个平台的原生通知系统提供了一组React Native API。

推送通知

推送通知从服务器开始发送。您的后端将消息传递给FCM,FCM再将消息送达设备,随后React Native应用会作出响应:

Your Backend
     │
     ▼
Firebase Cloud Messaging
     │
     ▼
Android / iOS Device
     │
     ▼
React Native Application

FCM区分通知消息(系统可代为显示)和数据消息(供您的代码使用的任意键值对数据),并且可以通过主题来定位单个设备或一组设备。

权限属于应用状态的一部分

安装应用不再意味着它一定会显示通知。从 Android 13 开始,POST_NOTIFICATIONS 成为一种运行时权限,针对 Android 13 及更高版本的应用在显示任何内容之前都必须请求此权限。iOS 则一直要求用户明确授权。应将权限视为可能变化的应用状态,而非一次性设置步骤。

合理的操作流程是先检查权限状态、说明其作用,然后再请求授权;无论用户给出何种答复,应用都能继续运行:

Application starts
       │
       ▼
Check notification permission
       │
       ├── Granted ───────► Continue
       │
       └── Not granted
                │
                ▼
        Explain the benefit
                │
                ▼
        Request permission
                │
          ┌─────┴─────┐
          ▼           ▼
       Granted      Denied
          │           │
          ▼           ▼
     Notifications   Continue

安装库文件

Notifee 负责显示通知:

npm install @notifee/react-native

React Native Firebase 提供核心应用模块以及用于推送消息的通信模块:

npm install @react-native-firebase/app
npm install @react-native-firebase/messaging

在 iOS 上,需随后安装本地的 CocoaPods 依赖项:

cd ios
pod install
cd ..

原生配置因 React Native 版本及项目设置的不同而有所差异,iOS 推送功能还需要在 Firebase 中配置 APNs 凭证,并在 Xcode 中开启推送功能。请遵循各库的最新文档说明。

显示第一条原生通知

第一个示例的目标是展示具有如下标题和内容的通知:

New Message
You have received a new message.

首先导入 Notifee 以及用于 Android 频道的优先级枚举类型:

import notifee, {
  AndroidImportance,
} from '@notifee/react-native';

下面的函数按顺序执行三项操作:请求权限,创建(或重用)名为 default 的 Android 频道,然后在该频道上显示通知。id 为 default 的 pressAction 用于指示 Android 在用户点击通知时打开应用;如果没有此配置,点击操作将不会产生任何效果。

export async function displayNotification() {
  // Request permission
  await notifee.requestPermission();  // Create Android notification channel
  const channelId = await notifee.createChannel({
    id: 'default',
    name: 'Default Notifications',
    importance: AndroidImportance.DEFAULT,
  });  // Display notification
  await notifee.displayNotification({
    title: 'New Message',
    body: 'You have received a new message.',    android: {
      channelId,
      pressAction: {
        id: 'default',
      },
    },
  });
}

反复调用createChannel是安全的;现有频道只会返回其ID。Android 8.0(API 26)及更高版本要求必须使用频道。可通过按钮触发该函数:

<Button
  title="Show Notification"
  onPress={displayNotification}
/>

请求Android 13权限

该Android权限的完整名称为:

android.permission.POST_NOTIFICATIONS

React Native在PermissionsAndroid中将其作为常量提供:

PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS

还必须在AndroidManifest.xml中使用uses-permission声明该权限,否则请求将会被默许拒绝。首先需要导入相关文件:

import {
  PermissionsAndroid,
  Platform,
} from 'react-native';

在iOS以及Android 13版本(API级别33)及更低版本中,由于默认允许通知,该辅助函数会立即返回true。否则它会显示系统对话框,并说明结果是否为GRANTED:

export async function requestAndroidNotificationPermission() {
  if (Platform.OS !== 'android') {
    return true;
  }  if (Platform.Version < 33) {
    return true;
  }  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
  );  return result === PermissionsAndroid.RESULTS.GRANTED;
}

从某个界面调用它:

const granted =
  await requestAndroidNotificationPermission();

然后根据返回结果进行分支处理:

if (granted) {
  console.log('Notifications allowed');
} else {
  console.log('Notifications denied');
}

Android可以返回三种有意义的结果:

GRANTED
DENIED
NEVER_ASK_AGAIN

NEVER_ASK_AGAIN需要特别关注。在多次被拒绝后,系统将不再显示该对话框,只有系统设置才能改变这一状态。

在恰当的时机询问

在第一个界面就弹出系统提示是一个常见且代价高昂的错误。新用户一看到

"Allow MyApp to send notifications?"

就没有任何背景信息,很可能会拒绝。应先进行说明,比如在欢迎界面上:

Welcome to MyApp

接着展示一张卡片,说明各种通知的用途,并附上一个按钮:

Enable notifications to receive:
✓ New messages
✓ Order updates
✓ Important alerts[Enable Notifications]

只有点击该按钮才会触发操作系统的对话框,这样请求就是响应用户的选择而非强行中断。

使用Notifee进行iOS授权

在 iOS 上,用户必须明确授权通知推送。Notifee 通过 AuthorizationStatus 返回授权结果:

import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';

下面的辅助函数将两种状态视为成功。AUTHORIZED 表示完全授权。PROVISIONAL 是 iOS 的静音模式,此时通知会直接发送到通知中心而不会打扰用户,用户之后可以选择保留或关闭这些通知。其他所有状态均视为拒绝:

export async function requestIOSNotificationPermission() {
  const settings = await notifee.requestPermission();  if (
    settings.authorizationStatus ===
    AuthorizationStatus.AUTHORIZED
  ) {
    console.log('Notifications authorized');
    return true;
  }  if (
    settings.authorizationStatus ===
    AuthorizationStatus.PROVISIONAL
  ) {
    console.log('Notifications provisionally authorized');
    return true;
  }  console.log('Notifications denied');  return false;
}

iOS 的请求可以包含多种选项:

Alerts
Sounds
Badges
CarPlay
Announcements
Provisional notifications

只请求实际需要的权限;例如,临时授权可以完全跳过提示流程。

为整个应用设置一个权限函数

不必在多个界面中重复进行权限检查,只需使用一个服务模块即可:

src/
 ├── services/
 │    └── notificationService.ts
 ├── screens/
 ├── components/
 └── App.tsx

服务文件:

// notificationService.ts

结合了两种平台的路径。在 Android 上,它会进行版本检查并发起运行时请求;在 iOS 上,则使用 Notifee,并接受已授权和临时状态:

import {
  Platform,
  PermissionsAndroid,
} from 'react-native';import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';export async function requestNotificationPermission() {
  if (Platform.OS === 'android') {
    if (Platform.Version < 33) {
      return true;
    }    const result = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );    return result === PermissionsAndroid.RESULTS.GRANTED;
  }  const settings = await notifee.requestPermission();  return (
    settings.authorizationStatus ===
      AuthorizationStatus.AUTHORIZED ||
    settings.authorizationStatus ===
      AuthorizationStatus.PROVISIONAL
  );
}

随后各界面会调用同一个函数:

const enabled = await requestNotificationPermission();

并根据返回结果采取相应操作:

if (enabled) {
  console.log('Ready for notifications');
}

较新版本的 Notifee 还可以通过 requestPermission() 在 Android 13 上请求 POST_NOTIFICATIONS;如果希望为两个平台使用统一调用方式,请查阅当前的 Notifee 文档。

Android 通知渠道

Android 8.0 引入了通知渠道,以便用户能够独立控制不同类别的通知。一个应用可以设置多个这样的渠道:

MyApp
│
├── Messages
├── Orders
├── Promotions
└── Security

每个渠道都有各自的重要程度和提示音。通常重要消息应被设置为高重要性,这样它们就会立即显示:

const messageChannel = await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
  importance: AndroidImportance.HIGH,
});

在更新订单时可以使用默认级别:

const orderChannel = await notifee.createChannel({
  id: 'orders',
  name: 'Orders',
  importance: AndroidImportance.DEFAULT,
});

通知会注明其所属的频道。该调用分为两部分显示,首先是标题和正文:

await notifee.displayNotification({
  title: 'New Message',
  body: 'You received a new message.',

接着是参考createChannel返回的频道ID的Android相关选项:

  android: {
    channelId: messageChannel,
  },
});

频道是面向用户的,而不仅仅是开发者的设置。用户可以在系统设置中静音或屏蔽某个频道,且一旦频道创建完成,应用便无法再通过编程方式更改其重要性;这一选择权在用户手中。请谨慎选择,或者稍后使用新的频道ID。

在实际应用中,一种有效的结构是根据功能对频道进行分类:

notifications
│
├── messages
├── orders
├── payments
├── security
└── marketing

独立的营销频道能让用户关闭促销信息,同时仍能收到安全警报。

FCM的定位

在推送模式下,FCM位于您的后端与每个已安装的应用之间:

┌─────────────────┐
             │   Your Backend  │
             └────────┬────────┘
                      │
                      ▼
             ┌─────────────────┐
             │      FCM        │
             └────────┬────────┘
                      │
              ┌───────┴───────┐
              ▼               ▼
        ┌──────────┐     ┌──────────┐
        │ Android  │     │   iOS    │
        └──────────┘     └──────────┘

获取设备令牌

每次应用安装后都会获得一个FCM注册令牌,您的后端需要该令牌来定位特定设备。请导入消息模块:

import messaging from '@react-native-firebase/messaging';

然后请求该令牌:

export async function getFCMToken() {
  const token = await messaging().getToken();  console.log('FCM Token:', token);  return token;
}

在真实应用中,获取令牌后会立即将其发送到您的服务器:

const token = await messaging().getToken();
await fetch('https://api.example.com/device-token', {
  method: 'POST',  headers: {
    'Content-Type': 'application/json',
  },  body: JSON.stringify({
    token,
  }),
});

随后,后端会将该令牌与登录用户及设备信息关联存储:

User
  │
  ├── userId
  ├── deviceId
  └── fcmToken

保持令牌最新

例如在重新安装或清除应用数据后,令牌会发生变化。需监听刷新事件:

import messaging from '@react-native-firebase/messaging';

监听器会将每个新令牌转发过来,同时函数会返回一个取消订阅的回调函数:

export function subscribeToTokenRefresh() {
  return messaging().onTokenRefresh(async token => {
    console.log('New FCM token:', token);    await sendTokenToBackend(token);
  });
}

这是一个最简的上传辅助函数,同样分为两部分,用于执行JSON POST请求:

async function sendTokenToBackend(token: string) {
  await fetch('https://api.example.com/device-token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },    body: JSON.stringify({
      token,
    }),
  });
}

Firebase的建议是随时处理令牌的变更,并将新值推送到您的应用服务器。在服务器端,还需移除FCM标记为无效的令牌,以避免过时的设备持续存在。

前台消息不会自动显示

最容易混淆的情况是当应用处于打开状态时收到推送消息:

User
  │
  ▼
React Native App
  │
  ▼
FCM message

监听前台消息:

import messaging from '@react-native-firebase/messaging';
const unsubscribe = messaging().onMessage(
  async remoteMessage => {
    console.log('Foreground message:', remoteMessage);
  },
);

然而,接收消息与显示系统通知是两个独立的步骤。当应用处于前台时,FCM只会将消息传递给您的代码,而不会自动显示任何内容。若要显示通知,需将内容传递给Notifee:

import notifee from '@notifee/react-native';

下面的处理程序会确保通道存在,然后显示通知;如果消息中没有notification块(例如仅包含数据的消息),则会使用默认文本作为替代:

messaging().onMessage(async remoteMessage => {
  const channelId = await notifee.createChannel({
    id: 'messages',
    name: 'Messages',
  });  await notifee.displayNotification({
    title:
      remoteMessage.notification?.title ??
      'New Message',    body:
      remoteMessage.notification?.body ??
      'You have a new message.',    android: {
      channelId,
      pressAction: {
        id: 'default',
      },
    },
  });
});

你可以控制信息的呈现方式,包括在相关界面已经显示时跳过该提示。

后台及退出状态消息

当应用处于后台时,其行为会发生变化。在Android上,FCM通知消息会直接显示在系统托盘中,而不会触发你的前台回调;那些同时包含通知内容和数据内容的消息则遵循各自的后台处理规则。在iOS上,仅包含数据的后台消息为尽力送达型,可能会被系统延迟或限制发送频率。

React Native Firebase为后台消息提供了专门的处理程序:

import messaging from '@react-native-firebase/messaging';
messaging().setBackgroundMessageHandler(
  async remoteMessage => {
    console.log(
      'Background message:',
      remoteMessage,
    );
  },
);

一个常见的错误是在 React 组件内部注册此处理程序,如下所示:

function App() {
  messaging().setBackgroundMessageHandler(...);
  return <Navigation />;
}

后台唤醒时可能不会加载任何组件,因此处理程序根本不存在。应在入口点处注册它:

// index.js
import messaging from '@react-native-firebase/messaging';messaging().setBackgroundMessageHandler(
  async remoteMessage => {
    console.log(
      'Background message:',
      remoteMessage,
    );
  },
);

然后再注册根组件:

AppRegistry.registerComponent(
  appName,
  () => App,
);

响应通知点击

点击通常应引导用户前往特定位置:

Notification
     │
     ▼
User taps
     │
     ▼
Application opens
     │
     ▼
Order Details

假设消息的数据中包含订单编号:

{
  "orderId": "12345"
}

对于从后台恢复的应用,当用户点击通知时,onNotificationOpenedApp 会被触发。回调函数可从 remoteMessage.data 中读取该编号并据此进行导航:

messaging().onNotificationOpenedApp(
  remoteMessage => {
    const orderId =
      remoteMessage.data?.orderId;
    if (orderId) {
      // Navigate to order details
      console.log(
        'Open order:',
        orderId,
      );
    }
  },
);

当应用完全关闭时

如果应用被终止,流程如下:

App completely closed
        │
        ▼
User receives notification
        │
        ▼
User taps notification
        │
        ▼
App launches

在这种情况下,onNotificationOpenedApp不会被触发。相反,应在应用启动时仅请求一次用于启动该应用的通知:

const remoteMessage =
  await messaging().getInitialNotification();
if (remoteMessage) {
  const orderId =
    remoteMessage.data?.orderId;  console.log(
    'App opened from notification:',
    orderId,
  );
}

这两种方法仅能处理FCM显示的通知。而通过Notifee自行展示的通知,比如上述的前台通知,则是通过Notifee自身的事件API(onForegroundEvent、onBackgroundEvent以及getInitialNotification)传递的。仅处理其中一种类型的通知是导致“点击无反应”故障的常见原因。

用于导航的有效载荷

仅包含文字内容的通知

New Order

无法告诉应用用户应跳转至何处。应在可见文本旁添加结构化数据:

{
  "notification": {
    "title": "New Order",
    "body": "Order #12345 has been confirmed"
  },
  "data": {
    "type": "order",
    "orderId": "12345"
  }
}

路由函数可以将type映射到对应的界面:

function handleNotification(data: any) {
  if (data.type === 'order') {
    navigation.navigate(
      'OrderDetails',
      {
        orderId: data.orderId,
      },
    );
  }
}

由于通知处理程序通常在组件外部运行,这类辅助函数中的navigation一般是应用根级创建的导航引用。关于通过深度链接处理路由点击的详细内容,请参阅React Native中通过深度链接处理路由推送通知点击。

检测安装后的权限变化

权限并非恒定不变。用户可能在周一允许接收通知:

Monday:
Notifications = ON

而在周三通过系统设置关闭它们:

Wednesday:
User opens Android SettingsNotifications = OFF

应用应当能够察觉到这一变化。Notifee会在不提示的情况下读取当前设置:

import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';
export async function checkNotificationPermission() {
  const settings =
    await notifee.getNotificationSettings();  if (
    settings.authorizationStatus ===
    AuthorizationStatus.AUTHORIZED
  ) {
    return true;
  }  return false;
}

请注意,此检查对于临时授权会返回 false;如果您的应用将其视为已启用,请添加 PROVISIONAL。当应用回到前台时需重新执行此检查。

用户拒绝时

不要重复提示,而应显示明确的状态,例如横幅:

Notifications are disabled

并附上可进入设置页面的操作:

Enable notifications from Settings
to receive important updates.[Open Settings]

Notifee 提供可直接打开应用通知设置的 API。所有相关原则可概括在一张卡片上:

Don't force
Don't spam
Explain the value
Give the user control

集中管理通知设置

在正式代码中,应将所有与通知相关的内容放在几个定义明确的模块中:

src/
│
├── services/
│   ├── notificationService.ts
│   └── firebaseMessaging.ts
│
├── navigation/
│   └── notificationNavigation.ts
│
├── screens/
│
└── App.tsx

主服务:

// notificationService.ts

提供了一个初始化函数,该函数会请求用户授权,若用户拒绝则停止操作,否则创建默认通道:

import notifee, {
  AndroidImportance,
  AuthorizationStatus,
} from '@notifee/react-native';export async function initializeNotifications() {
  const settings =
    await notifee.requestPermission();  const authorized =
    settings.authorizationStatus ===
      AuthorizationStatus.AUTHORIZED ||
    settings.authorizationStatus ===
      AuthorizationStatus.PROVISIONAL;  if (!authorized) {
    return false;
  }  await notifee.createChannel({
    id: 'default',
    name: 'Default Notifications',
    importance: AndroidImportance.DEFAULT,
  });  return true;
}

从组件中调用它非常简单:

useEffect(() => {
  initializeNotifications();
}, []);

注意:在第一个界面就会立即弹出此提示,即之前提到的时间问题。应改为从同意选项按钮处调用它。

后端部分

典型的数据模型会将用户及其设备存储在不同的表中,因为一个用户可能拥有多部手机或平板电脑:

users
│
├── id
├── name
└── email
user_devices
│
├── id
├── user_id
├── device_id
├── fcm_token
├── platform
├── created_at
└── updated_at

设备注册记录可能如下所示:

{
  "userId": 101,
  "deviceId": "device-abc",
  "fcmToken": "xxxxx",
  "platform": "android"
}

当业务事件发生时,后端会查找用户的令牌并通过FCM发送消息:

Order Created
      │
      ▼
Backend
      │
      ▼
Find user's FCM token
      │
      ▼
Firebase Cloud Messaging
      │
      ▼
User's phone

FCM要求消息必须从可信环境发送,比如应用程序服务器或Cloud Functions,绝不能从客户端发送,因为发送消息需要服务器凭证。

可扩展的有效载荷

设计每个有效载荷时,要让应用能够理解事件本身而不仅仅是文本。例如,支付确认信息可以包含:

{
  "notification": {
    "title": "Payment Successful",
    "body": "Your payment of ₹999 was successful."
  },
  "data": {
    "type": "payment",
    "paymentId": "PAY12345",
    "screen": "PaymentDetails"
  }
}

通过一个基于data.type的switch语句即可处理所有类型的通知:

switch (data.type) {
  case 'payment':
    navigation.navigate(
      'PaymentDetails',
      {
        paymentId: data.paymentId,
      },
    );
    break;
  case 'order':
    navigation.navigate(
      'OrderDetails',
      {
        orderId: data.orderId,
      },
    );
    break;
}

每增加一种通知类型就需新增一个处理案例。由于FCM的数据值为字符串,因此需要显式解析数字。

完整架构

将各部分整合后,实际生产环境中的设置如下:

┌──────────────────┐
                    │    Backend API   │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │ Firebase / FCM   │
                    └────────┬─────────┘
                             │
                ┌────────────┴────────────┐
                │                         │
                ▼                         ▼
        ┌──────────────┐          ┌──────────────┐
        │    Android   │          │     iOS      │
        └──────┬───────┘          └──────┬───────┘
               │                         │
               ▼                         ▼
        Notification                 APNs / FCM
          Channel                        │
               │                         │
               └────────────┬────────────┘
                            ▼
                    React Native App
                            │
                            ▼
                    Navigation / UI

在iOS平台上,FCM本身并不直接传递消息,而是通过Apple Push Notification服务(APNs)来转发消息,因此必须配置APNs的凭证。

常见错误

首次启动时直接弹出提示

在首屏直接显示内容而不作任何解释,这是最常见的错误:

useEffect(() => {
  requestNotificationPermission();
}, []);

应先说明相关数值,再在用户操作后请求权限。

忘记设置频道

如果没有设置频道,Android 8.0及更高版本将不会显示通知。请在展示内容前先创建频道:

await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
});

期望始终显示前台推送通知

虽然使用了

messaging().onMessage(...)

这种方式,但并不代表在所有情况下都会出现可见的系统通知。为确保始终以前台形式展示通知,建议使用Notifee自行显示。

忽视令牌变化

一旦保存的令牌失效,相关功能就会停止工作。应使用

messaging().onTokenRefresh(...)

并将每个新生成的令牌发送到后端。

在组件中注册后台处理程序

如前所述,后台处理程序应位于入口点处,即在 React 树结构之外。

忽略后续的权限变更

用户可以随时撤销权限或屏蔽某个特定频道。您的检查逻辑需涵盖所有这些状态:

Granted
Denied
Changed externally
Channel blocked
App notifications disabled

敏感数据泄露

通知文本会显示在锁屏上,以及设备附近的任何人都能看到。与其

Your bank account ending in 1234 received ₹50,000.

发送无关内容:

New account activity
Tap to view details.

不如在用户完成应用内身份验证后再显示详细信息。

测试检查清单

由于行为会因操作系统版本、应用状态及设备不同而有所差异,因此需进行系统化的测试。在 Android 上:

☐ Android 12
☐ Android 13+
☐ Permission granted
☐ Permission denied
☐ Permission disabled from Settings
☐ Channel enabled
☐ Channel disabled
☐ App foreground
☐ App background
☐ App killed
☐ Notification tapped
☐ Multiple notifications

在 iOS 上:

☐ Permission granted
☐ Permission denied
☐ Permission changed from Settings
☐ App foreground
☐ App background
☐ App terminated
☐ Notification tapped
☐ Badge
☐ Sound
☐ Deep linking

而对于 FCM 集成的端到端测试:

☐ Token generated
☐ Token stored on backend
☐ Token refresh handled
☐ Foreground message received
☐ Background message received
☐ Notification click handled
☐ Initial notification handled

运行时流程概览

从应用启动到内容传递再到导航的完整流程如下:

APP START
                       │
                       ▼
              Check permissions
                       │
              ┌────────┴────────┐
              │                 │
           Allowed            Denied
              │                 │
              ▼                 ▼
       Initialize FCM      Show explanation
              │                 │
              ▼                 ▼
       Get FCM token       Request permission
              │
              ▼
       Send token to API
              │
              ▼
          App Running
              │
       ┌──────┴──────┐
       │             │
       ▼             ▼
   Foreground     Background
       │             │
       ▼             ▼
    onMessage      FCM/APNs
       │             │
       ▼             ▼
    Notifee       System UI
       │             │
       └──────┬──────┘
              ▼
        User taps
              │
              ▼
       Read notification data
              │
              ▼
          Navigation

本地通知与推送通知,以及二者的结合使用

这两种通知类型在几个方面存在明显差异:

  • 触发源:本地通知来自应用或设备本身;推送通知则来自后端服务器。
  • 网络需求:本地通知通常可在离线状态下使用;推送通知则需要网络连接。
  • FCM需求:本地通知不需要FCM;推送通知通常需要它。
  • 定时提醒:更适合使用本地通知;并非推送通知的典型用途。
  • 服务器事件、订单更新、聊天消息:推送通知是更常见的选择;本地通知只能以有限方式处理这些内容。
  • 相关库:本地通知可用Notifee;推送通知则需要FCM加上显示相关的库。

它们也能很好地结合使用。常见的架构是让FCM发送消息,你的代码处理消息,再由Notifee展示出来:

FCM receives message
        ↓
React Native processes message
        ↓
Notifee displays native notification

用户在每一步应看到的内容

进行手动验证时,需清楚预期效果。在Android 13及以上版本中,会出现系统对话框:

Allow MyApp to send you notifications?

在应用的系统通知设置中,你创建的频道会以独立的开关形式呈现:

MyApp
 ├── Messages
 ├── Orders
 └── Promotions

在iOS上,会出现系统授权提示,而发送的通知会显示在通知中心。对于推送功能,Firebase控制台可以直接向设备令牌发送测试消息,这是区分后端错误与客户端错误的最快方法:

Firebase Console
      ↓
Cloud Messaging
      ↓
Test Message
      ↓
FCM Token

最后,点击订单通知后:

Notification
     ↓
Order #12345

应跳转至对应的页面:

Order Details Screen

生产环境最佳实践

合理安排权限请求的时间,让用户明白通知为何有用,而非仅在应用启动时才请求。应将相关逻辑集中在一个服务中,避免在多个界面重复实现:

notificationService.ts

将不同类型的通知分为不同的渠道或类别,例如:

messages
orders
payments
security
marketing

设计导航所需的数据包,在适当情况下包含以下字段:

type
id
screen

除此之外:

  • 将令牌与已认证的用户和设备绑定,安全存储并处理刷新机制。
  • 避免在通知文本中包含敏感信息,因为这些内容会在应用外部显示。
  • 要在真实设备上进行测试,因为模拟器和仿真器的表现可能有所不同。

总结

一个功能完善的通知系统远不止是一个简单的调用而已

showNotification();

正是所有这些因素的结合:

Permissions
     +
Notification Channels
     +
FCM Tokens
     +
Foreground Handling
     +
Background Handling
     +
Notification Clicks
     +
Deep Linking
     +
Backend Integration

关键在于明确的职责划分。FCM负责传递消息,操作系统掌管权限及原生界面展示,Notifee则让React Native能够控制本地界面展示。后端决定何时向谁发送何种消息。将它们视为一个整体系统,其结构如下:

┌─────────────────┐
              │   Backend API   │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │      FCM        │
              └────────┬────────┘
                       │
              ┌────────┴────────┐
              ▼                 ▼
        ┌───────────┐     ┌───────────┐
        │  Android  │     │    iOS    │
        │  Channel  │     │   APNs    │
        └─────┬─────┘     └─────┬─────┘
              │                 │
              └────────┬────────┘
                       ▼
                React Native
                       │
                       ▼
                 Notifee
                       │
                       ▼
                Native UI

有了这样的边界划分,通知在Android和iOS上,以及在前台、后台和已终止状态下都能保持一致的行为。

快速参考

申请Android权限:

await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);

申请iOS权限:

await notifee.requestPermission();

创建Android频道:

await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
});

显示通知:

await notifee.displayNotification({
  title: 'New Message',
  body: 'You have a new message.',
  android: {
    channelId: 'messages',
  },
});

监听前台FCM消息:

messaging().onMessage(async message => {
  console.log(message);
});

监听令牌变化:

messaging().onTokenRefresh(token => {
  console.log(token);
});

处理从后台打开的通知:

messaging().onNotificationOpenedApp(message => {
  console.log(message.data);
});