欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

PWA实战准备篇一

程序员文章站 2022-05-24 19:11:57
...

PWA实战准备篇

这一篇的内容是利用ngrok让我们的服务跑在https上,然后利用一个boilerplate快速搭建PWA的环境

让我们的Service Worker运行起来

在讲PWA拦截网络请求之前,我们先说一下如何让我们的
Service Worker能够运行起来,因为支持https,这里有几种办法

  • 使用github来托管代码
  • 使用localhost,但这种有局限性,有时候我们不太方便用localhost
  • 使用FireBase来托管代码
  • 使用ngrok来启动服务

这里主要介绍第四种办法,使用ngrok来启动服务,使用方法:

  • 下载ngrok https://dashboard.ngrok.com/get-started
  • 解压后安装,加入环境变量
  • 在我们静态资源的文件夹,启动http服务,例如我用的是http-server的一个库,默认端口是8080
    PWA实战准备篇一

  • 然后启动ngrok来进行转发,ngrok http 8080
    PWA实战准备篇一

能看到上面图片,在浏览器中访问https://1e517654.ngrok.io 就可以啦,手机也可以哟,不一定要在局域网

boilerplate *

这里我们用了一个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中的推送通知的流程

PWA实战准备篇一

首先浏览器会显示一个提示来询问用户是否愿意接受通知,如果接受,可以将用户订阅的详细信息保存在服务器上,稍后用来发送通知,因为这些信息对于每个用户、设备和浏览器都是唯一的,所以如果一个用户使用了多个设备登录了网站,那么每台设备都会提示用户是否接受通知。

一旦用户接受了通知,服务器就可以使用这些订阅的信息来向用户发送消息,并且使用一个调度任务来向用户分发实时信息。

我们来看一个实际的例子:

浏览器端的代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>test pwa push</title>
        <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,国内目前是看不到做的必要

通过上面的例子可以实现firefox上的推送
电脑上的截图:
PWA实战准备篇一