这一篇的内容是利用ngrok让我们的服务跑在https上,然后利用一个boilerplate快速搭建PWA的环境
在讲PWA拦截网络请求之前,我们先说一下如何让我们的
Service Worker能够运行起来,因为支持https,这里有几种办法
这里主要介绍第四种办法,使用ngrok来启动服务,使用方法:
能看到上面图片,在浏览器中访问https://1e517654.ngrok.io 就可以啦,手机也可以哟,不一定要在局域网
这里我们用了一个boilerplate,链接地址:https://github.com/vuejs-templates/pwa
我们先看一下目录
.
├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── load-minified.js
**│ ├── service-worker-dev.js
│ ├── service-worker-prod.js**
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
**│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js**
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── dist
│ ├── index.html
│ ├── service-worker.js
│ └── static
│ ├── css
│ │ └── app.1d063bc0cd301699760e884e1e4c3379.css
│ ├── img
│ │ └── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── msapplication-icon-144x144.png
│ │ ├── mstile-150x150.png
│ │ └── safari-pinned-tab.svg
│ ├── js
│ │ ├── app.ba6cfb06f7fd7ba74300.js
│ │ ├── app.ba6cfb06f7fd7ba74300.js.map
│ │ ├── manifest.2ae2e69a05c33dfc65f8.js
│ │ ├── manifest.2ae2e69a05c33dfc65f8.js.map
│ │ ├── vendor.1134bd10c2663336acdb.js
│ │ └── vendor.1134bd10c2663336acdb.js.map
│ └── manifest.json
├── index.html
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── Hello.vue
│ ├── main.js
│ └── router
│ └── index.js
├── static
│ ├── img
│ │ └── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── msapplication-icon-144x144.png
│ │ ├── mstile-150x150.png
│ │ └── safari-pinned-tab.svg
│ └── manifest.json
└── yarn.lock
大部分的内容和我们之前用vue是一样的,这里主要是说一下和pwa相关的东西
但是说实话,如果是从学习pwa的角度,我们完全可以自己去搭环境,没必要用这个boilerplate。当然用它好处在于我们去关心业务逻辑,而不是让我们搭环境就搭个半天。
项目使用webpack来编译打包,同样我们的 Service Worker 也用了webpack的插件:sw-precache-webpack-plugin
我首先回忆一下之前的内容,我们如果需要缓存一个html和css或者说js,我们需要再sw.js中一个个的添加,这种操作对我们现代前端来说是不科学的,能不能有工具,让我们设置一个数组,他就自动写好了呢?
这个插件就是上面说的sw-precache-webpack-plugin
new SWPrecacheWebpackPlugin({
cacheId: 'upchat-pwa',
filename: 'service-worker.js',
staticFileGlobs: ['dist/**/*.{js,html,css}'],
minify: false,
stripPrefix: 'dist/'
})
其中的staticFile Globs就是缓存需要的东西,其他还有很多选项,有兴趣大家可以去查看,通过这个插件会自动生成一个server-worker.js,里面就有我们之前写到的监听install、fetch的事件。
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
// 省略
});
)
});
然后我们还有一个mainfest.json,里面定义了web应用的图标、启动页面和样式
{
"name": "upchat-pwa",
"short_name": "U聊",
"icons": [
{
"src": "/static/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
有了fetch、版本控制、manifest,那么推送怎么办呢?我们U聊肯定是需要推动的,用户不可能一直在U聊的界面上,这个稍微复杂一点,且听我慢慢道来。
因为是推送,我们肯定是有推送的服务器,像ios有苹果的推送服务器,android上也有华为和小米的推送服务器。当然如果你想自己去写一个推送服务器也不是不可以,但是考虑到浏览器的多样性,这里我建议还是用的第三方SaaS产品,比如OneSignal、Roost和Aimtell都有成熟的解决方案。我们接下来就看如何把OneSignal集成到我们的boilerplate里面去。
在说集成之前,我们先回顾一下PWA中的推送通知的流程
首先浏览器会显示一个提示来询问用户是否愿意接受通知,如果接受,可以将用户订阅的详细信息保存在服务器上,稍后用来发送通知,因为这些信息对于每个用户、设备和浏览器都是唯一的,所以如果一个用户使用了多个设备登录了网站,那么每台设备都会提示用户是否接受通知。
一旦用户接受了通知,服务器就可以使用这些订阅的信息来向用户发送消息,并且使用一个调度任务来向用户分发实时信息。
我们来看一个实际的例子:
浏览器端的代码:
<html>
<head>
<meta charset="UTF-8">
<title>test pwa pushtitle>
<link rel="manifest" href="/manifest.json">
head>
<body>
<script>
var endpoint;
var key;
var authSecret;
var vapidPublickey = '公钥和服务器约定';
function urlBase64ToUint8Array(base64string) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/\-/, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for(let i = 0; i < rawData.length; i++) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(function(registration) {
return registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return;
}
return registration.pushManager.subscription({
userVisibileOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublickey)
})
.then(function(subscription) {
var rawKey = subscription.getKey? subscription.getKey('p256dh'): '';
key = rawKey? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))): '';
var rawAuthSecret = subscription.getKey? subscription.getKey('auth'): '';
authSecret = rawAuthSecret? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))): '';
endpoint = subscription.endpoint;
return fetch('./register', {
method: 'post',
headers: new Headers({
'content-type': 'application-json'
}),
body: JSON.stringify({
endpoint: endpoint,
key: key,
authSecret: authSecret,
}),
})
})
});
}).catch(function(err) {
console.log('注册失败')
});
}
script>
body>
html>
要发送推送通知,需要使用VAPID协议,VAPID协议是自主应用服务器的简称。本质上定义了应用服务器和推送服务器之间的握手,并允许推送服务确认那个站点正在发送消息。这是很重要的,因为这意味着应用服务器能够包含其自身的相关附加信息,这些信息可用于联系应用服务器的操作人员。
代码中有一个VAPID的公钥,这个公钥我们在服务器代码中详细解释
后续的代码就是注册Service Worker,然后检测用户是否订阅过了,如果订阅过了就需要再给服务器发送消息,否则用pushManager.subscribe函数来提示用户订阅,这个函数使用VAPID公钥识别,这个需要转换为UInt8Array,因为规范只支持这种类型。最后用POST请求发送给服务器
服务器代码:
const webpush = require('web-push');
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(
'mailto:[email protected]',
'对应的公钥',
'不清楚'
);
app.post('/register', function(req, res) {
var endpoint = req.body.endpoint;
// saveRegistrationDetails(endpoint, key, authSecret);
const pushSubscription = {
endpoint: endpoint,
keys: {
auth: req.body.authSecret,
p256dh: req.body.key
}
};
var body = 'Thank you';
var iconUrl = 'https://ddd.png;';
webpush.sendNotification(pushSubscription,
JSON.stringify({
msg: body,
url: 'http://localhost:3111',
icon: iconUrl,
})
).then(result => res.sendStatus(201))
.catch(err => console.log(err));
});
app.listen(3111, function(){
console.log('listen on 3111');
});
有了服务器代码之后我们需要再前端添加一点代码,让浏览器能够接受通知,并且能够操作
self.addEventListenser('push', function(event) {
var payload = event.data? JSON.parse(event.data.text()): 'no payload';
var title = 'Progressive Times';
event.waitUtil(
self.registeration.showNotification(title, {
body: payload.msg,
url: payload.url,
icon: payload.icon
});
);
});
self.addEventListenser('notificationclick', function(event) {
event.notification.close(); // 点击后关闭通知
//检查当前窗口是否打开了,如果没有就切换到这个窗口
event.waitUtil(
clients.matchAll({
type: 'window'
})
.then(function(clientList) {
for(var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if(client.url == '/' && 'focus' in client) {
return client.focus();
}
if (client.openWindow) {
return client.openWindow('http://localhost:3111') //单击后代开URL
}
}
})
);
});
但是就算你们把上面的代码,写好了,还是会发现,返回的是500,因为国内的网络被墙了,在chrome中打开,后,pushManager.getSubscription()
是不需要push Service信息的,也就是浏览器去哪个push service注册是浏览器内部定义好的,不是外部可以修改的。也就是说第三方push service永远无法给chrome、firefox这些已有push service的浏览器下发消息。
因为web push不仅仅需要业务服务器,还需要push中间服务器,chrome下就是fcm
在chrome下:
"endpoint": "https://fcm.googleapis.com/fcm/send/fBAoBL13tiw:APA91bGlEPl5USKrU9OurtYvS9ljEemAh1r6Hxr2UPWNhnBd29exJ4h7_lL_yfPucVWRF5CzQzP7knPPp-6N1CnQL0gacxUnBN5w0HP5tbaHH58uqxLDEke8qEXUvIpMd4fowivmD3mMBRDNb33tth6CKWATeEOLmg"
在firefox下
"endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABbY-_5v3kCWvAzOcu0YUiQ-DWJCpb3PfAMMwtqQmhxRSqZjbqORsQrON6P6iYi3A84l96repM6V5KkLuO9Yucb3Vjt27HtqYIUXQh0ELUeXpB-c_gN2H0qJ9szXvqYjuOW_MW-Q5lQTffgRRd5vjzumlYGWCh0fmKztmMNnM0EHi87hDA"
国内浏览器不支持web push,就算按照w3c标准实现了push service,说服浏览器厂商来用难度太大。chrome被墙,国内推送无望。所以只能给firefox推送消息(safari走的自己的协议,定制下也能推送;opera没有自己的push service,用的fcm),在国内大部分用户都是用360、qq、搜狗、2345的情况下,覆盖量实在是少的可怜。所以第三方web push,国内目前是看不到做的必要