使用Service Worker的 Web Push 功能

现代的 Android 应用和 iOS 应用都有相应的推送系统,可以给用户推送消息。即使用户没有打开这个应用,也同样可以收到消息。当然,网页既然要向手机应用靠拢的话,那推送功能也是必不可少。 今天我们就来介绍一下,网页如何实现类似的推送功能。

# 基于 Service Worker 的推送功能

Service Worker 提供了两种推送服务,一种是基于 Firebase Cloud Messaging 的推送功能,一种是基于 VAPID 的推送功能。第一种已经过时了,所以本文只介绍第二种。

# Create a public/private key pair

首先,我们需要创建公钥和私钥,类似与 HTTPS 的 RSA 加密。客户端使用公钥订阅推送,服务端使用私钥来发送通知。创建既可以在浏览器进行,也可以使用 Node 创建。当然,推荐在后端生成更安全。

前端生成代码参考链接:util.ts (opens new window)

后端使用web-push (opens new window)可以更方便的生成公钥和私钥。代码如下:

function generateVAPIDKeys() {
  const vapidKeys = webpush.generateVAPIDKeys();

  return {
    publicKey: vapidKeys.publicKey,
    privateKey: vapidKeys.privateKey
  };
}
1
2
3
4
5
6
7
8

# Subscribing with the public key

接下来我们需要订阅推送:

navigator.serviceWorker.ready
  .then(registration => {
    return registration.pushManager.getSubscription().then(subscription => {
      if (subscription === null) {
        return registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: this.vapidPublicKey
        });
      } else {
        return subscription;
      }
    });
  })
  .then(subscription => {
    this.subJSON = subscription.toJSON();
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们使用公钥进行订阅,并且把订阅之后的内容打印出来,方便服务器使用。实际工程中需要发送给服务器进行保存。使用 Chrome 浏览器订阅的信息是这样的:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/d3cMmjx0Eg8:APA91bF1i7wgLJRw-VgOh3Evn6RG1xqdOR6Y0CeTUm1xiD36BCHXaDoceVfilDYiifWdI_rWdU8IdJjqSxaCVscRp5zl9lon8u4mf9mha0fmSVKJzUOx5r5Jba2yiNmCFRxxKcTJm51S",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDtGmJB0Bkyum0WJw8NiiCn4U9ckX8UjhzXPUad2HM0yID0ced8zUHKr-Yhf6p2Z7IS0G07dGG7Tnl5jlwQVog4",
    "auth": "25JGSYqTKvj_nAeodHBHSQ"
  }
}
1
2
3
4
5
6
7
8

我们可以看到推送的网址是 Chrome 提供的服务,Firefox 浏览器下订阅的信息是这样的:

{
  "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABghCrItYXGJw0eCyp0Ae1lTxMXkb6Fxhg8tRck2cMoY4bZjvkV2j5t95FfrPftdieUgeaNthjmb0_XyoIVqWIy7cpy9lMjczHb5TYpC7sKnOw4IekwrtQbmBo6Vn54TZaUSrBIb40PEy2KXF5QlyOj2QxlTz6d6NPB6mMvJxuYNSg-5xs",
  "keys": {
    "auth": "MCeQnXlaz4A-CBuALNPbcQ",
    "p256dh": "BL4am0lzx005spT_UBbMagfWb93Cfgh8XtkCtP7y697dODFnO0wCVVI783BsiHePRTl-mrpoHolJ0gKTYR1T4SQ"
  }
}
1
2
3
4
5
6
7

endpoint 我们可以看出推送服务本身是浏览器提供的,如果无法访问这个网址,自然就无法使用推送服务。

# Server push

接下来,我们让服务端发送一个消息试试:

const webpush = require('web-push');

const vapidPublicKey = 'public key';

const vapidPrivateKey = 'private key';

// PushSubscriptionJSON Info
const pushSubscription = {
  endpoint: '',
  expirationTime: null,
  keys: {
    p256dh: '',
    auth: ''
  }
};

const payload = 'hello world!';

const options = {
  vapidDetails: {
    subject: 'mailto:example@web-push-node.org',
    publicKey: vapidPublicKey,
    privateKey: vapidPrivateKey
  },
  TTL: 60
};
webpush
  .sendNotification(pushSubscription, payload, options)
  .then(function(result) {
    console.log('success!');
    console.log(result);
  })
  .catch(function(err) {
    console.log('fail!');
    console.error(err);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

在上述代码中填入相应的认证信息,就可以使用了。浏览器的推送功能并不需要把相应的网站打开,只要浏览器是打开的就可以收到推送。

# 总结

浏览器的推送功能非常方便,但是首先需要用户授权通知权限才能使用,其次是浏览器提供的推送服务,可能在中国无法正常使用。所以现在这个功能还没有大规模的应用。

# 参考

Introduction to Push Notifications (opens new window)

Adding Push Notifications to a Web App (opens new window)

Service Worker Push Notifications Demo (opens new window)

前端开发中的加载性能优化

不同于需要安装的安卓 App 或者 iOS App,前端 App 所有的资源文件都要通过网络进行下载,所以加载速度越快对用户的体验来说就越好。因为我们也需要在加载的时候进行优化,减少不必要的资源请求。

# 文本内容的优化

# 压缩代码

HTML,JS,CSS 都有相应的工具可以进行代码压缩,可以使用 webpack 工具方便的进行代码压缩。JS 使用工具terser (opens new window),CSS 使用CSS Nano (opens new window), HTML 使用HTMLMinifier (opens new window)进行代码压缩。

# 使用 GZIP 进行压缩

现代化浏览器都支持 GZIP 进行压缩,并且服务器端 NGINX 也可以很方便的配置使用 GZIP 压缩请求的资源文件。

# 减少第三方 JS 的引用

在 2010 年左右,人们都爱使用 jQuery 框架进行开发,因为 jQuery 抹平了浏览器之间的差异性,大大提高了开发效率。但随着浏览器吸收了 jQuery 的各种优点,例如可以使用document.querySelector很方便的像 jQuery 一样选择元素。现在的前端开发已经不那么需要 jQuery 了,你可以访问You might not need jQuery (opens new window)来查找替代的方法。

# webpack 相关的优化

可以使用 webpack 的SplitChunksPlugin (opens new window), 分割 JS 文件,只打包首次载入需要的 JS 文件来减少网络请求。 我们也可以使用Tree Shaking (opens new window)来引入需要的 JS 库,避免不使用的代码被打包进来。

# 图像内容的优化

# 移除不使用的图像

由于网站项目的更新,很多时候有一些不被使用的图像资源被打包进输出目录。这样会导致无谓的资源加载,需要及时发现和删除。

# 选择合适的图像格式

众所周知,png 图像可以展示透明效果,但是 jpg 图像做不到。但是因为 png 比 jpg 多了一个 alpha 通道,所以占用的空间也大,如果不需要透明效果展示,相同的图像效果,jpg 会占用更小的空间。

# 移除图像的 Metadata

图像的 Metadata 对于 web 资源来说并没有什么意义,所以移除 Metadata 可以减少图像占用空间。

# 调整图像尺寸

如果显示的尺寸和实际尺寸差距过大,也会造成不必要的资源浪费。

# 调整图像质量

在很多情况下,对图像质量没有过高的要求,可以损失质量来换取更小的空间。

# 压缩图像

现在有很多算法可以有损或者无损的压缩图像,在发布之前,可以使用压缩工具减少图像的使用空间。

# HTTP 请求优化

可以使用 HTTP2 协议来提高网站的载入速度,HTTP2 允许多路复用,多个请求可以使用单一的 HTTP 连接来处理,大大提高了载入速度。 这里有一个demo (opens new window) 大家可以使用浏览器打开,感受一下。

# HTTP Cache

可以通过缓存技术来提高网站的载入速度,当用户再次访问网站的时候可以直接使用缓存而不必再次请求资源文件。我们甚至可以使用Service Workers (opens new window)缓存整个网站,保证可以离线使用。

# 总结

载入优化需要从各个方面入手,查找分析前端 App 的资源文件,并从网络请求和缓存下手才能达到最优的效果。

# 参考

Loading Performance (opens new window)

vue实现datepicker组件

前端组件化的今天,选择日期组件算是最复杂的一个了吧。我们需要考虑日期显示的多语言,弹框的显示位置,以及日期月份的计算。

# 日期相关计算

计算每个月的天数,这个是不变的。一三五七八十腊,三十一天永不差;四六九冬三十整,惟有二月二十八,闰年还要把一日加。

function getDayCountOfMonth(year: number, month: number) {
  if (month === 3 || month === 5 || month === 8 || month === 10) {
    return 30;
  }

  if (month === 1) {
    if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
      return 29;
    } else {
      return 28;
    }
  }

  return 31;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

计算每个月的 1 号是周几:

function getFirstDayOfMonth(date: Date) {
  const temp = new Date(date.getTime());
  temp.setDate(1);
  return temp.getDay();
}
1
2
3
4
5

其他计算函数可以去看完整代码date.ts (opens new window)

# 弹窗定位相关

点击日期输入框,需要弹出下拉菜单,下拉菜单位置的计算并不简单,需要考虑到浏览器的窗口,如果输入框在下方,需要在上方弹出,如果输入框在上方,需要在下方弹出。 并且用户滑动到可视范围以外的话,还要可以隐藏。所以我们这里引用了一个比较成熟的第三方库来解决这个问题。 popperjs (opens new window) 这个库可以帮我们解决上面的问题,节约开发的成本。

# 多语言支持

日期选择组件,是为数不多需要做多语言支持的组件。因为我们要显示月份和每周的名字。就不得不做多语言处理了。目前我做的 demo 里面包含了三种语言的支持。

# 总结

日期选择组件,常见的需求有单日期选择和日期范围选择。优秀的组件需要同时满足这两个需求。并且尽量保证代码的简洁性。为此,我在 Github 写了一个 demo 库,希望可以给大家参考。

vue-datepicker (opens new window)