上一篇文章我们分享了 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 推送通知
欢迎吐槽,共同进步