-
Notifications
You must be signed in to change notification settings - Fork 6
PWA 落地实践
前面几篇文章大概介绍了 service worker 的底层原理,这里,我们通过实例来看看,如何自己做好一个 service worker 的应用。先说一下我这项目的大致架构,借由 react-redux-starter-kit 作为基础底层开发环境(这真是个神库,各种开发配置帮你写好了,自己就不用当轮子哥了,改一改就能用了),上层是 React + Redux 来写。废话就不说了,我们直接开始吧。
这里一块,主要利用 @media
的相关属性来对页面做响应式的优化,具体为:
@media (min-width : 375px) and (max-width : 667px) and (resolution : 2dppx){
html{font-size: 37.5px;}
}
//iphone5 的基准值
@media (min-width : 320px) and (max-width : 568px) and (resolution : 2dppx){
html{font-size: 32px;}
}
然后利用 rem
单位进行相关长度的设置。例如在:components/Content/content.scss
中,定义相关形状的大小:
@import 'base';
.icon-cw{
right: px2rem(6px);
top: px2rem(3px);
&::before{
width:px2rem(32px);
}
}
.icon-plus{
bottom:px2rem(6px);
right:px2rem(6px);
}
其中,px2rem
是根据自定义函数来写的一个转换函数。
@function px2rem($px){
$rem : 37.5px;
@return ($px/$rem) + rem;
}
这一部分也没多少可以讲的,具体参考前面那篇响应式文章即可。
这里,主要是将 SW 的主要功能给实现一遍,主要有 Cache
,Push
,Notification
,Message
。我们一个一个来:
因为 SW 不是天生就是可以使用的,需要经过用户允许,所以,我们需要首先在 index.js
中写入:
SW.register('sw.js').then(function (registration) {
// doSth
}).catch(function (err) {
});
这样,先请求一下权限。OK 之后,就到了用户注册阶段。这里,我们就需要在 sw.js 中,进行 install
的相关操作了。
const CACHE_VERSION = 1;
const CURRENT_CACHES = {
prefetch: 'prefetch-cache-v' + CACHE_VERSION
};
self.addEventListener('install', function(event) {
// 缓存指定的文件
const urlsToPrefetch = [
'vendor.js'
];
event.waitUntil(
caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
var cachePromises = urlsToPrefetch.map(function(urlToPrefetch) {
var url = new URL(urlToPrefetch,location.origin); // 拼路径
console.log('now send the request to' + url);
var request = new Request(url);
return fetch(request).then(function(response) {
if (response.status >= 400) {
throw new Error('request for ' + urlToPrefetch +
' failed with status ' + response.statusText);
}
return cache.put(urlToPrefetch, response);
}).catch(function(error) {
console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
});
});
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching complete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
经过上面的 prefetch
我们会创建一个 cache-object
用来存放指定域名的缓存文件。之后,如果有其他请求发送的话,就会接着触发 fetch
事件。
有时候,我们嫌自己硬编码在 fetch 里面 code
比较累人,那么建议可以直接在 fetch 中对相关的资源做处理。这里提供一种思路:
- 根据 pathname 来判断是否缓存,比如只缓存
/
的资源文件。 - 根据 method 来判断是否缓存,比如只缓存
GET
的资源文件。 - 根据 contentType 来判断是否缓存,比如只缓存
js/css/png
文件。
OK,那么我们接下来就开始做吧。
importScripts('./path-to-regexp.js');
const FILE_LISTS = ['js','css','png'];
const PATH_FILE = '/:file?'; // 缓存接受的路径文件
var goSaving = function(url){
for(var file of FILE_LISTS){
if(url.endsWith(file)) return true;
}
return false;
}
// 判断 path/method/contentType
function checkFile(request){
var matchPath = pathtoRegexp(PATH_FILE);
var url = location.pathname;
var method = request.method.toLowerCase();
url = matchPath.exec(url)[1];
return !!(goSaving(url) && method === 'get');
}
self.addEventListener('fetch', function(event) {
// 检查是否需要缓存!!!!!!!!很重要!!!!!
if(!checkFile(event.request))return;
event.respondWith(
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(response) {
console.log('save file:' + location.href);
// 需要缓存,则将资源放到 caches Object 中
return caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
上面的代码缓存部分大话,大家看前面的文章就 OK。这里,需要给大家普及一点:fetch
是会捕获所有的请求,如果你直接 return
回去,则相当于 bypass
,不会对请求有任何影响! 所以,对不是指定资源的文件,直接通过网络获取。其余的采取有缓存就给,没缓存则向远端拉取。
这里,根据 path-to-reg 来对 pathname
做相关的处理。另外,现在也有比较高级的库,比如 Google 的 sw-tool。不过,本人认为 SW 能够做的事情比较少,如果在额外用一个比较重的库,可能回有点麻烦,不过,这也得取决于具体的业务。
不过,这也会涉及到一个问题,如何更新 /
下的文件,即,document
文件? 这里我们可以通过 message
来做一层 document
的文件的懒更新:
// 在 index.js 中
if (SW.controller) { // 这是当前页面的 `controller`
console.log('send message ::');
SW.controller.postMessage("fetch document")
}
然后在 SW 中,接收相关的指令,触发更新:
self.addEventListener('message',event =>{
console.log("receive message" + event.data);
// 更新根目录下的 html 文件。
var url = self.location.href;
console.log("update root file " + url);
event.waitUntil(
caches.open(CURRENT_CACHES.prefetch).then(cache=>{
return fetch(url)
.then(res=>{
cache.put(url,res);
})
})
)
});
缓存讲完了,之后,就该到我们的 Notification 阶段。
Notification 也不是一开始就具备的,这也需要用户的同意才行:
Notification.requestPermission();
同意了之后,我们可以先在 SW 中做个测试:
function sendNote(){
console.log('send Note');
var title = 'Yay a message.';
var body = 'We have received a push message.';
var icon = '/student.png';
var tag = 'simple-push-demo-notification-tag'+ Math.random();
var data = {
doge: {
wow: 'such amaze notification data'
}
};
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: data,
actions:[
{
action:"focus",
title:"focus"
}]
})
}
这里,一个简单的 notification
是额外需要相关的资源的,比如 ICON。ICON 的话,在网上随便找一个配一配就行。那我们如何模拟像 APP 推送,能够打开 APP 这种行为呢?很简单,我们只要在 SW 中监听 notificationclick
即可:
function focusOpen(){
var url = location.href;
clients.matchAll({
type:'window',
includeUncontrolled: true
}).then(clients=>{
for(var client of clients){
if(client.url = url) return client.focus(); // 经过测试,focus 貌似无效
}
console.log('not focus');
clients.openWindow(location.origin);
})
}
self.addEventListener('notificationclick', function(event) {
var messageId = event.notification.data;
event.notification.close();
if(event.action === "focus"){
focusOpen();
}
});
上面那种判断方法是根据 PC 端的推送指定的,通过 Note 中的 actions
属性,来进行不同的行为。不过,在手机端上,我们一般使用 openWindow
即可,因为手机端无法显示 action
。
// 手机端上处理
self.addEventListener('notificationclick', function(event) {
var messageId = event.notification.data;
event.notification.close();
clients.openWindow(location.origin);
});
如果测试通过,那么童鞋,恭喜你,可以进入 web-push
阶段了。
Web Push 前面也说过,是一个比较复杂的过程,首先要生成 pair keys,接着通过客户端订阅,然后才能发送。生成 key 的话,我这里就不想详述了,前面也已经说过了。我们直接开始订阅这一块:
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.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;
}
subscribe() {
var key = this.urlBase64ToUint8Array('BPLISiRYgXzzLY_-mKahMBdYPeRZU-8bFVzgJMcDuthMxD08v0cEfc9krx6pG5VGOC31oX_QEuOSgU5CYLqpzf0');
navigator.serviceWorker.ready.then(reg=> {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey:key
})
.then(subscription=> {
return fetch('/subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
})
})
}
其中的 key
是我们之前用 web-push
生成的。我们通过 subscribe()
会自动得到本次订阅独一无二的描述,当然,我们之前需要检查一下订阅状态,如果已经订阅了,就没必要重复订阅了。
navigator.serviceWorker.ready.then(reg=>{
reg.pushManager.getSubscription().then(sub=>{
if(sub)return;
this.subscribe();
})
})
这里,不经需要前端做,后台也需要存储该次订阅的信息。后台的话,就使用 express4.x 来做。然后收到订阅的同时,立即发送一次 push
给前台。
const webpush = require('web-push');
webpush.setVapidDetails(
'mailto:villainthr@gmail.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
app.route('/subscription')
.post((req,res,next)=>{
console.dir("receive subscribe : " + req.body);
console.log(JSON.stringify(req.body));
// 返回数据给前端
res.json({status:'ok'});
// 立即,push 一次信息
webpush.sendNotification(req.body,"ok")
.then(info=>{
console.log('发送成功:' + info)
})
.catch(err =>{
console.log(err);
})
})
之后,我们在 sw.js
中的 push
事件中做相关处理即可。
self.addEventListener('push', function(event) {
sendNote(event.data)
});
function sendNote(push){
console.log('send Note');
var title = 'Yay a message.';
var body = push;
var icon = '/student.png';
var tag = 'simple-push-demo-notification-tag'+ Math.random();
var data = {
doge: {
wow: 'such amaze notification data'
}
};
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: data,
actions:[
{
action:"focus",
title:"focus"
}]
})
}
不过,你能不能成功还是要看运气的。因为,我们一直活在墙中,如果你使用 Chrome
的话。它的 message server 你是连不上的 (;′⌒‘)。 所以,关于 push
还需要等国内的浏览器跟上才行。
那么到这里,SW 做的所有工作,都已经做完了。剩下最后一点的就是如何将 Web 添加到桌面。这个就需要 Manifest 的帮助,前面已经讲过我这里就不赘述了。通过 webmanifest.st 生成自己网站的 Manifest,然后在 HTML 中,通过 link
标签引入:
<link rel="manifest" href="/manifest.webmanifest">
如果 Chrome
检测到上述 link
标签,那么就会提示你是否愿意添加到桌面。如果你想对此做相关的优化,可以直接使用 beforeinstallprompt
事件。
var deferredPrompt;
window.addEventListener('beforeinstallprompt', function(e) {
console.log('beforeinstallprompt Event fired');
e.preventDefault();
deferredPrompt = e;
return false;
});
btnSave.addEventListener('click', function() {
if(deferredPrompt !== undefined) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(function(choiceResult) {
console.log(choiceResult.outcome);
if(choiceResult.outcome == 'dismissed') {
// 拒绝添加
console.log('User cancelled home screen install');
}
else {
console.log('User added to home screen');
}
// 不在提醒
deferredPrompt = null;
});
}
});
这里,我简单实践了一下,最后是可以成功添加到桌面上的。
该项目已经放到 github 上了。具体使用也很简单:
// 下载依赖包
npm install
期间你会遇见一些关于包的问题,最大的问题就是 node-sass。大家自己去下一个对应的编译包放进去就行。
没问题之后,就可以运行:
npm start
访问:localhost:3000 即可查看。