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

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

App 入口

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

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

因为我们使用 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 推送通知

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

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


已发布

分类

来自

评论

欢迎吐槽,共同进步

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