【系列教程】使用 Vercel Serverless function 连接 APNs 实现 iOS 推送通知(2)代码解析
上一篇文章我们分享了 Push Notification 的基础原理和项目配置,这一篇我们开始看具体的代码。
App 入口
我们在 App 入口里主要做两件事:
- 设置通知类型
- 侦听用户点击通知的动作,以便跳转到特定页面
因为我们使用 Expo 作为基础框架,所以我们只需要把这部分代码放在 app/_layout.tsx 里面即可。与 Expo 的例子不同,我发现直接处理跳转可能会失败,因为我的手机比较旧,系统会经常性自动关闭 App,所以点击通知的时候,跳转可能发生在应用初始化完成之前。于是我通过侦听 rootNavigationState 的变化来确定跳转的时机,实测效果不错。
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 密钥:
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,发送消息即可:
// 因为 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 推送通知
相关文章
【系列教程】使用 Vercel Serverless function 连接 APNs 实现 iOS 推送通知(3)时区处理
上一篇博客我们分析基于 APNs 实现 iOS Push Notification 的代码,讲解关键环节的关键 […]
React Native + Expo 入门级实战开发多平台应用 WhiteScreen:3. 深入开发,完成应用主体
感谢剪辑同学的努力工作,第三集上线。 油管地址:https://youtu.be/0Ix-Y-MPQY0 B站 […]
【视频教程】React Native + Expo 入门级实战开发多平台应用 WhiteScreen:2. 配置模拟器开发环境+开发Expo应用
感谢剪辑同学的努力工作,第二集终于可以奉献给各位同学。 油管地址:https://youtu.be/YEPvi […]


