【系列教程】使用 Vercel Serverless function 连接 APNs 实现 iOS 推送通知(2)代码解析

上一篇文章我们分享了 Push Notification 的基础原理和项目配置,这一篇我们开始看具体的代码。

App 入口

我们在 App 入口里主要做两件事:

  1. 设置通知类型
  2. 侦听用户点击通知的动作,以便跳转到特定页面

因为我们使用 Expo 作为基础框架,所以我们只需要把这部分代码放在 app/_layout.tsx 里面即可。与 Expo 的例子不同,我发现直接处理跳转可能会失败,因为我的手机比较旧,系统会经常性自动关闭 App,所以点击通知的时候,跳转可能发生在应用初始化完成之前。于是我通过侦听 rootNavigationState 的变化来确定跳转的时机,实测效果不错。

app/_layout.tsx
Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }), }); export default function RootLayout() { const router = useRouter(); const rootNavigationState = useRootNavigationState(); const notification = useRef<Notifications.Notification>(); const isMounted = useRef<boolean>(false); const notificationListener = useRef<Notifications.EventSubscription>(); // 重定向到指定页面 function redirect(notification: Notifications.Notification) { // 注意这里的 `content.body` 将来用得到 const intro = notification.request.content.body; if (intro) { router.push(`/compose/?reminder=${intro}` as Href); } } useEffect(() => { // 复制来的代码这两个部分有点重合,但是测试起来很麻烦,所以我就都留着了。 Notifications.getLastNotificationResponseAsync() .then(response => { // 这里只记录通知的内容,因为可能需要一些时间完成启动初始化,所以把跳转放到下面的 useEffect 里处理 if (!isMounted.current || !response?.notification) { notification.current = response?.notification; return; } redirect(response?.notification); }); const subscription = Notifications .addNotificationResponseReceivedListener(response => { // 同上 if (!isMounted.current || !response.notification) { notification.current = response.notification; return; } redirect(response.notification); }); return () => { notificationListener.current && Notifications.removeNotificationSubscription(notificationListener.current); subscription.remove(); isMounted.current = false; }; }, []); // 处理跳转,放到这里比较稳定 useEffect(() => { if (!rootNavigationState?.key) return; isMounted.current = true; if (notification.current) { redirect(notification.current); notification.current = undefined; } }, [rootNavigationState]); return ( <页面组件 /> ); }

跳转的目标页面

因为 Expo 帮我们封装了路由,所以目标页面的处理非常简单,直接使用获取全局路由参数的方法 useGlobalSearchParams() 即可。如果你使用别的框架,或者原生 React Native,也无非就是在不同位置保存跳转要携带的参数,然后在目标页使用而已。所以这个部份我就省略了。

发送通知

发送通知的代码不在 App 里。如前篇文章所述,我们的服务器端代码部署在 Vercel Serverless,通过 cronjob 定时调用,在用户设定的时间发送通知。其实使用 node.js 的话,发送通知的代码是比较简单的。使用 deno 的话,可能会卡在生成 jwt 签名那里,我没有成功。如果有哪位同学比较熟 deno,可以指导我一下。

首先,创建环境变量:

APNS_TOKEN="-----BEGIN PRIVATE KEY-----
在 Apple 开发者后台创建密钥之后,把 p8 文件复制到这里。
换行没有问题
-----END PRIVATE KEY-----"
APPLE_APP_BUNDLE_ID=
APNS_TEAM_ID=
APNS_KEY_ID=

接下来,计算 JWT 密钥:

push-notification.ts
import jwt from 'jsonwebtoken'; const authToken = jwt.sign( { iss: process.env.APNS_TEAM_ID, iat: Math.round(Date.now() / 1000), }, process.env.APNS_TOKEN as string, { header: { alg: 'ES256', kid: process.env.APNS_KEY_ID, typ: undefined, // 这个东西也很重要,有些范例代码里没写 }, }, );

最后,请求 APNS,发送消息即可:

push-notification.ts
// 因为 Apple 要求 http2,所以不能使用 fetch 发送请求,必需使用 node.js http2 模块 // 又因为 node.js 模块使用事件侦听器,于是必须用 Promise 包起来才能确保请求完整发出,否则执行完代码,serverless 不会等待,会直接关闭请求 // 然后我们就会看到一堆成功的请求,但是并没有真的发出 push notification await new Promise((resolve) => { const client = http2.connect('https://api.push.apple.com'); const headers = { ':method': 'POST', ':scheme': 'https', 'apns-topic': process.env.APPLE_APP_BUNDLE_ID as string, ':path': '/3/device/' + token, authorization: `bearer ${authToken}`, } const request = client.request(headers); request.setEncoding('utf8'); request.write(JSON.stringify({ aps: { alert: { title, body: content, }, }, })); // 这里几个事件侦听器基本只做调试。只有下面的 `end` 影响到执行 request.on('response', (headers, flags) => { console.log('Response:', headers, flags); }); let data = ''; request.on('data', (chunk) => { data += chunk; }); request.on('end', () => { console.log('End:', data); client.close(); resolve('ok'); }); request.end(); });

小结

基本上,使用 Serverless function 发送 Push notification 和在应用端接收消息并跳转到指定页面的核心代码就如上所示。这里面有一些坑,我也是踩了一天才解决。

希望对大家有帮助。如果各位同学有什么问题,可以留言提出。下一篇会介绍时区处理,便于大家全球化。

本站目前仍在招商中,感兴趣的老板请与我联系。


【系列教程】使用 Vercel Serverless function 连接 APNs 实现 iOS 推送通知

  1. 基础知识
  2. 代码解析(本文)
  3. 时区处理

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

欢迎吐槽,共同进步

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理