首页 / 文章 / 在 React Native 中通过深度链接发送推送通知点击事件

在 React Native 中通过深度链接发送推送通知点击事件

设置通用链接和应用链接,将 Pusher Beams 集成到 React Native 应用中,并将网页 URL 映射到原生界面,以便点击通知时能打开正确的页面。

1898 词

只有当点击推送通知后用户能直接进入其对应的页面时,该通知才有用。试想一个为国际学生提供大学选择服务的React Native应用:电子邮件可能无法被读取,而短信在不同国家之间也不可靠,因此当招聘人员发送消息时,推送通知必须能够正常使用。在这里,你将需要在两个平台上配置深度链接,为每个Pusher Beams通知关联一个网站URL,并将该URL转换为React Navigation的路由,同时逐步测试每一环节。

为何深度链接能确定通知的跳转目标

在构建此方案时,此处使用的 Pusher 包无法将通知中的自定义数据传递给 Android 平台上的 React Native 应用的 JavaScript 端。最简单的解决办法是向 Android SDK 提交 拉取请求,即利用深度链接功能:Android 通知中会包含一个普通网站链接,用户点击该链接后,应用现有的深度链接处理机制会决定打开哪个界面。

此后这一情况可能已有所变化,因此请先查看当前的 Beams SDK 版本。无论怎样,下面的路由层都很实用,因为它也能处理来自邮件、短信或聊天的链接。

深度链接简述

深度链接功能允许应用获取指向用户自己网站的链接,并以原生方式打开这些链接。像 https://test.com/message/abc 这样的链接应该直接在应用内打开对应的 abc 消息,而非启动浏览器。iOS将这类链接称为通用链接,Android则称之为应用链接,两种系统都要求用户的域名需上传一份文件来证明其信任该应用。

在两个平台上设置深度链接

在 .well-known 目录下上传验证文件

在网站根目录下创建一个 /.well-known/ 目录。对于Android,需添加 /.well-known/assetlinks.json 文件,该文件声明您的应用有权处理该域名下的所有URL。请将文件中的占位符替换为您应用的包名以及用于签名该文件的证书的SHA-256指纹:

[{
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
        "namespace": "android_app",
        "package_name": "package_name",
        "sha256_cert_fingerprints":
        ["package_cert_fingerprint"]
    }
}]

如果 Google Play 对您发布的应用重新签名,请使用 Play 签名密钥的指纹,而非上传密钥的指纹,否则仅在生产环境中验证会失败。

对于 iOS,需添加一个名为 /.well-known/apple-app-site-association(无扩展名)的文件。该文件需列出由团队 ID 和应用包 ID 组成的应用标识符,以及应用应拦截的 URL 路径:

{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "appId",
                "paths": [ "/paths-you-want-to-support", "/messenger"]
            }
        ]
    }
}

通过相同域名通过 HTTPS 提供这两个文件,且不得使用重定向。这是传统的 applinks 结构;苹果后来增加了新的格式,因此请查阅最新的文档。

在 iOS 上启用关联域名

iOS还需要一个权限设置,用于指定哪些域名属于该应用。在Xcode中打开Capabilities,开启Associated Domains选项,然后添加每个需要关联的域名,通常格式为applinks:yourdomain.com

在Android上声明意图过滤器

在 Android 系统中,深度链接是通过 AndroidManifest.xml 中的意图过滤器来声明的。若要处理 /messenger/* 格式的链接,主活动需要一个针对 VIEW 操作、带有 DEFAULTBROWSABLE 类别的意图过滤器,同时还需一个 data 元素,用于指定 https 协议、主机地址以及 /messenger 作为路径前缀。添加 android:autoVerify="true" 可让系统检查 assetlinks.json,从而使应用无需弹出选择对话框即可打开这些链接。

在根组件中监听链接

应用程序的根组件需要完成两项任务:一是读取应用在首次启动时所使用的URL,二是监听应用运行过程中传入的URL。React Native的Linking模块通过getInitialURL()函数和url事件监听器实现了这两项功能。这两个功能都应调用同一个处理函数,目前该函数只需将URL记录下来即可:

const handleDeepLink = (url: string) => console.log(url);

在添加通知功能前先测试链接

在为深度链接功能叠加推送通知之前,应先单独验证深度链接是否正常工作。一种简单的方法是通过Slack等消息应用在每台测试设备上发送链接给自己,然后点击该链接。如果配置正确,链接会打开你的应用而非浏览器,并且记录下来的URL应与你点击的链接一致。将这一功能独立出来,这样日后如果出现通知路径错误的问题,就能确定只是推送功能出了问题。

在应用中添加 Pusher Beams

前提条件与社区桥接方案

您需要一个Pusher Beams账户,并且必须完成针对 Apple 推送服务及 Google Firebase 的相关设置步骤。

需注意,在撰写本文时 Pusher 尚未提供官方的 React Native 包。此处使用的桥接方案为社区开发的 react-native-pusher-push-notifications 包,它能将原生 Beams SDK 与 JavaScript 相连接,但并非 Pusher 官方支持。为使其能在此配置中正常运行,需要对该包进行少量调整。在 Android 端,主要的改动是对 Pusher 的 push-notifications-android SDK 进行了分支修改,使得点击通知即可打开深度链接。这种非官方的桥接方案加上经过分支处理的 SDK 需要持续维护:需固定版本号,并在官方 SDK 发生变化时重新评估选择。

在 iOS 上安装 Swift SDK

iOS 端依赖于 Pusher 的 Swift SDK,可通过 Carthage 进行安装。在 /ios/Cartfile 中添加以下行,然后运行 carthage bootstrap 以下载并构建该 SDK:

github "pusher/push-notifications-swift" ~> 1.3.0

该版本是当时此集成所使用的最新版本;新的项目可能会使用更新版本的 SDK,甚至可能采用其他的依赖管理工具。

由于原生部分需要定制,因此需按照桥接包为两个平台提供的手动安装步骤进行操作。

发送测试通知

iOS 上的通知仅会出现在真实设备上,而不会在模拟器中显示,因此请准备好一台真正的 iPhone。

要发送测试推送,可以使用 Pusher 控制面板中的调试控制台,或使用 Postman 等 HTTP 客户端调用 Beams 的发布 API。只需准备一个包含 iOS 部分和 Android 部分的请求体即可:iOS 部分设置徽章数为 5,Android 部分则填写需要打开的网站 URL。

当所有配置正确后,iOS 会直接处理通知内容,而 Android 则通过携带网站 URL 的 View intent 打开应用。在两个平台上,处理程序都应记录类似 https://yourdomain/messenger/abcde 的信息。在 iOS 上,应用图标还应显示数值为 5 的徽章。

将 URL 转换为 React Navigation 路由

最后一步是用真正的路由替换日志记录代码。常见的配置是在网页上使用 React Router,而在 React Native 应用中则使用 React Navigation。一个将网页路由映射到原生路由的小工具可以让两者共享逻辑,同时避免因网页 URL 名称变更而悄悄破坏应用导航功能。

在网页端与原生端之间共享路由常量

两个代码库都会将路由定义为常量。在网页端,路由构建器会返回具体的路径或 React Router 用于匹配的模式;而在原生端,则对应的是屏幕名称,对话 ID 会作为导航参数单独传递:

// Web:
ROUTE = {
    MESSENGER_CONVERSATION: (conversationId?: string) =>
        conversationId
            ? `/messenger/${conversationId}`
            : "/messenger/:conversationId"
}// Native:
APP_STACK_ROUTE: {
    MESSENGER_CONVERSATION_SCREEN:
        "app_stack_routes/messenger_conversation"
}
// native then has a params object with conversationId included

由于双方都用常量表示,因此只需在一个表格中即可说明哪个网页路径对应哪个原生屏幕:

const ROUTE_MATCHES: IRouteMatches = [
    {
        webPath: ROUTE.MESSENGER_CONVERSATION(),
        rnPath: APP_STACK_ROUTES.MESSENGER_CONVERSATION_SCREEN
    }
];

调用 ROUTE.MESSENGER_CONVERSATION() 且不传参数时会返回模式 /messenger/:conversationId,因此表格中存储的与网站路由器使用的模式相同。重命名网页路由会自动更新映射关系。如需了解这些网页模式的运作原理,可参阅 React Router基础

延迟处理传入的URL

有时每次点击都会触发多次处理逻辑,例如当初始URL检测和监听器都检测到链接时,就会导致多个界面同时出现。通过将每个URL传递给RxJS的 Subject 并应用 debounceTime(100),可以将连续的触发次数合并为一次调用,该调用随后会被传递给 processUrl

export const handleDeepLink = (url: string): void => {
    if (!url) return;
    onChangeUrl$.next(url);
};
const onChangeUrl$: Subject<string> = new Subject<string>();
const urlSubscription: Observable<string> = onChangeUrl$.pipe(debounceTime(100));
urlSubscription.subscribe(processUrl);

权衡之处在于:100 毫秒内出现的两个不同链接会合并为最后一个,这对于通知点击场景来说是可接受的。

将 URL 拆分成多个部分

processUrl 首先使用正则表达式将 URL 分解为协议、主机、路径和查询字符串(调整正则时可以使用RegExr这类工具)。解构操作会跳过完整匹配结果以及仍包含 ? 的查询组:

const REGEX_DECONSTRUCT_URL = /^(.*?):\/\/(.*?)(\/.*?)(\?(.*))?$/;const deconstructedUrl = REGEX_DECONSTRUCT_URL.exec(url);
    if (!deconstructedUrl) return;
    const [originalUrl, protocol, tld, path, ignore, querystring] = deconstructedUrl;

请注意,该模式要求存在路径:没有结尾斜杠的纯 https://yourdomain 格式不会匹配,函数会提前返回。这对通知链接来说没问题,但若在其他地方重复使用此辅助函数,则需了解这一点。

将路径与路由表进行匹配

提取出相关部分后,处理程序仅会对您域名下的 HTTPS 链接进行处理,然后依次检查 ROUTE_MATCHES 中的各个条目,直到有某个条目匹配该路径:

if (protocol === "https" && tld.includes("yourdomain")) {
    for (let i = 0; i < ROUTE_MATCHES.length; i++) {
        if (matchPath(ROUTE_MATCHES[i], path, querystring)) {
            // loop until one matches
            break;
        }
    }
}

tld.includes("yourdomain") 这种检查方式虽然方便,但精度较低:它甚至会接受类似 yourdomain.attacker.example 这样的主机地址。操作系统会验证通用链接和应用链接,因此风险相对有限,但一旦其他 URL 来源也共享同一个处理程序,使用精确的主机白名单就能显著降低风险。

matchPath 内部,会使用 React Router 所依赖的同一库 path-to-regexpwebPath 模式与传入的路径进行匹配。一旦匹配成功,提取出的参数(如 conversationId)就会成为对应 rnPath 页面的参数,随后应用会导航到该页面。由于此操作在任意组件之外执行,因此会由一个持有根导航器引用的小型导航服务来触发它,这正是 React Navigation 文档中关于无需使用 navigation 属性进行导航的描述。

总结

通过整合这些组件,应用可以通过同一条代码路径完成两项任务:

  • 无论链接来自邮件、短信还是聊天,都能在原生应用中打开对应的页面。
  • 推送通知使用相同的深度链接处理机制,因此用户只需点击即可直接进入对应的对话界面。
  • 保持其可维护性的关键在于将网站URL视为目标的唯一描述,通过一个表格将URL转换为原生路由。需分别测试每一层功能,加强主机检查,并关注官方的Beams SDK:如果它们将数据包转发给JavaScript,那么就可以放弃自行分支的开发,直接使用现有的路由层。

    相关阅读