美文网首页
H5 PWA技术

H5 PWA技术

作者: cain07 | 来源:发表于2021-12-01 14:54 被阅读0次

    1、原生app优缺点

    a、体验好、下载到手机上入口方便
      b、开发成本高(ios和安卓)
      c、软件上线需要审核
      d、版本更新需要将新版本上传到不同的应用商店
      e、使用前需下载

    2、web网页优缺点

    a、开发成本低、网站更新时上传最新的资源到服务器即可、手机自带浏览器打开即可
      b、体验比原生app差
      c、入口不便捷
      d、无网无相应,不具备离线能力
      e、无app的消息推送

    3、PWA是什么?

    PWA是一个新的前端技术,全称:Progressive Web App,这是一个渐进式的网页应用程序。结合了一系列现代web技术,在网页应用中实现和原生应用相近的用户体验。

    PWA的三个关键词:

    Reliable(可靠的):当用户从手机屏幕启动时,无需考虑网络状态,可以立刻加载出PWA
      Fast(快速的):加载速度快
      Engaging(可参与的):PWA可以添加在用户的主屏幕上,无需从应用商店里下载,他们通过网络应用程序Manifest file提供类似于APP的使用体验(android上可设置全屏显示,由于Safari支持度的问题ios不可以),可以进行“推送通知”

    小小总结:

    a、解决的问题:
          1>可添加至主屏幕
          2>实现离线缓存功能
          3>实现消息推送

    b、优势:几乎瞬间加载,但安全且富有弹性
      c、核心:manifest文件清单、Service Workers

    4、Manifest

    作用:  
      a、能够将你浏览的网页添加到你的手机屏幕上
      b、在 Android 上能够全屏启动,不显示地址栏 ( 由于 Iphone 手机的浏览器是 Safari ,所以不支持)
      c、控制屏幕 横屏 / 竖屏 展示
      d、定义启动画面
      e、可以设置你的应用启动是从主屏幕启动还是从 URL 启动
      f、可以设置你添加屏幕上的应用程序图标、名字、图标大小
    示例:
    index.html

    <head>
      <title>Minimal PWA</title>
      <meta name="viewport" content="width=device-width, user-scalable=no" />
      <link rel="manifest" href="manifest.json" />
      <link rel="stylesheet" type="text/css" href="main.css">
      <link rel="icon" href="/e.png" type="image/png" />
    </head>
    

    manifest.json

    {
      "name": "Minimal PWA", // 必填 显示的插件名称
      "short_name": "PWA Demo", // 可选  在APP launcher和新的tab页显示,如果没有设置,则使用name
      "description": "The app that helps you understand PWA", //用于描述应用
      "display": "standalone", // 定义开发人员对Web应用程序的首选显示模式。standalone模式会有单独的
      "start_url": "/", // 应用启动时的url
      "theme_color": "#313131", // 桌面图标的背景色
      "background_color": "#313131", // 为web应用程序预定义的背景颜色。在启动web应用程序和加载应用程序的内容之间创建了一个平滑的过渡。
      "icons": [ // 桌面图标,是一个数组
        {
        "src": "icon/lowres.webp",
        "sizes": "48x48",  // 以空格分隔的图片尺寸
        "type": "image/webp"  // 帮助userAgent快速排除不支持的类型
      },
      {
        "src": "icon/lowres",
        "sizes": "48x48"
      },
      {
        "src": "icon/hd_hi.ico",
        "sizes": "72x72 96x96 128x128 256x256"
      },
      {
        "src": "icon/hd_hi.svg",
        "sizes": "72x72"
      }
      ]
    }
    

    5、Service Worker**

    SW 是什么呢?这个是离线缓存文件。我们 PWA 技术使用的就是它!SW 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门,因为使用了它,才会有的那个 Reliable 特性吧,SW 作用于 浏览器于服务器之间,相当于一个代理服务器。

    • 基本特点

    • 运行在它自己的全局脚本上下文中

    • 不绑定到具体的网页

    • 无法修改网页中的元素,因为它无法访问 DOM

    • 只能使用 HTTPS

    • 拦截进出的 HTTP 请求,从而完全控制你的网站

    • 与主JS线程独立,不会被阻塞

    • 完全异步,无法使用localStorage

    功能(还是比较逆天的)

    • 后台数据的同步
    • 从其他域获取资源请求
    • 接受计算密集型数据的更新,多页面共享该数据
    • 客户端编译与依赖管理
    • 后端服务的hook机制
    • 根据URL模式,自定义模板
    • 性能优化
    • 消息推送
    • 定时默认更新
    • 地理围栏

    生命周期:

    image
    • Parsed ( 解析成功 ): 首次注册 SW 时,浏览器解决脚本并获得入口点,如果解析成功,就可以访问到 SW 注册对象,在这一点中我们需要在 HTML 页面中添加一个判断,判断该浏览器是否支持 SW 。

    • Installing ( 正在安装 ):SW 脚本解析完成之后,浏览器会尝试进行安装,installing 中 install 事件被执行,如果其中有 event.waitUntil ( ) 方法,则 installing 事件会一直等到该方法中的 Promise 完成之后才会成功,如果 Promise 被拒绝,则安装失败,SW会进入 Redundant( 废弃 )状态。

    • Installed / Waiting (安装成功/等待中):如果安装成功,SW 将会进入这个状态。

    • Activating ( 正在激活 ):处于 waiting 状态的 SW 发生以下情况,将会进入 activating 状态中:

      当前已无激活状态的 worker 、 SW脚本中的 self.skipWaiting()方法被调用 ( ps: self 是 SW 中作用于全局的对象,这个方法根据英文翻译过来也能明白什么意思啦,跳过等待状态 )、用户已关闭 SW 作用域下的所有页面,从而释放了当前处于激活状态的 worker、超出指定时间,从而释放当前处于激活状态的 worker

    • Activated ( 激活成功 ):该状态,其成功接收了 document 全面控制的激活态 worker 。

    • Redundant ( 废弃 ):这个状态的出现时有原因的,如果 installing 事件失败或者 activating 事件失败或者新的 SW 替换其成为激活态 worker 。installing 事件失败和 activating 事件失败的信息我们可以在 Chrome 浏览器的 DevTools 中查看

    如果上个图不好理解,可以看这个,把它的生命周期看成红绿灯:

    image
    • register (需要下载和解析,红灯)
    • install (执行,黄灯)
    • activated( 成功,绿灯)

    当用户首次导航至 URL 时,服务器会返回响应的网页

    • 第1步:当你调用 register() 函数时, Service Worker 开始下载。
    • 第2步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
    • 第3步:一旦 Service Worker 成功执行了,install 事件就会激活
    • 第4步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!

    6、实现离线缓存

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>Hello Caching World!</title>
      </head>
      <body>
        <!-- Image -->
        <img src="/images/hello.png" />                 
        <!-- JavaScript -->
        <script async src="/js/script.js"></script>     
        <script>
          // 注册 service worker
          if ('serviceWorker' in navigator) {           
            navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
              // 注册成功
              console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch(function (err) {                   
              // 注册失败 :(
              console.log('ServiceWorker registration failed: ', err);
            });
          }
        </script>
      </body>
    </html>
    

    注意:

    注:Service Worker 的注册路径决定了其 scope 默认作用页面的范围。
    如果 service-worker.js 是在 /sw/ 页面路径下,这使得该 Service Worker 默认只会收到 页面/sw/ 路径下的 fetch 事件。
    如果存放在网站的根路径下,则将会收到该网站的所有 fetch 事件。
    如果希望改变它的作用域,可在第二个参数设置 scope 范围。示例中将其改为了根目录,即对整个站点生效。
    service-worker.js

    var cacheName = 'helloWorld';     // 缓存的名称  
    // install 事件,它发生在浏览器安装并注册 Service Worker 时        
    self.addEventListener('install', event => { 
    /* event.waitUtil 用于在安装成功之前执行一些预装逻辑
     但是建议只做一些轻量级和非常重要资源的缓存,减少安装失败的概率
     安装成功后 ServiceWorker 状态会从 installing 变为 installed */
      event.waitUntil(
        caches.open(cacheName)                  
        .then(cache => cache.addAll([    // 如果所有的文件都成功缓存了,便会安装完成。如果任何文件下载失败了,那么安装过程也会随之失败。        
          '/js/script.js',
          '/images/hello.png'
        ]))
      );
    });
      
    /**
    为 fetch 事件添加一个事件监听器。接下来,使用 caches.match() 函数来检查传入的请求 URL 是否匹配当前缓存中存在的任何内容。如果存在的话,返回缓存的资源。
    如果资源并不存在于缓存当中,通过网络来获取资源,并将获取到的资源添加到缓存中。
    */
    
    self.addEventListener('fetch', function (event) {
      event.respondWith(
        caches.match(event.request)                  
        .then(function (response) {
          if (response) {                            
            return response;                         
          }
          var requestToCache = event.request.clone();  //          
          return fetch(requestToCache).then(                   
            function (response) {
              if (!response || response.status !== 200) {      
                return response;
              }
              var responseToCache = response.clone();          
              caches.open(cacheName)                           
                .then(function (cache) {
                  cache.put(requestToCache, responseToCache);  
                });
              return response;             
        })
      );
    });
    

    注意:

    注:为什么用request.clone()和response.clone()
    需要这么做是因为request和response是一个流,它只能消耗一次。因为我们已经通过缓存消耗了一次,然后发起 HTTP 请求还要再消耗一次,所以我们需要在此时克隆请求
    Clone the request—a request is a stream and can only be consumed once.

    7、消息推送

    步骤一、提示用户并获得他们的订阅详细信息
    步骤二、将这些详细信息保存在服务器上
    步骤三、在需要时发送任何消息
    前两步:

    index.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>Progressive Times</title>
        <link rel="manifest" href="/manifest.json">                                      
      </head>
      <body>
        <script>
          var endpoint;
          var key;
          var authSecret;
          var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
          // 方法很复杂,但是可以不用具体看,知识用来转化vapidPublicKey用
          function 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;
          }
          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.subscribe({                              
                      userVisibleOnly: 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: subscription.endpoint,
                          key: key,
                          authSecret: authSecret,
                        }),
                      });
                    });
                });
            }).catch(function (err) {
              // 注册失败 :(
              console.log('ServiceWorker registration failed: ', err);
            });
          }
        </script>
      </body>
    </html>
    

    步骤三:

    app.js

    const webpush = require('web-push');                 
    const express = require('express');
    var bodyParser = require('body-parser');
    const app = express();
    webpush.setVapidDetails(                             
      'mailto:contact@deanhume.com',
      'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
      'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
    );
    app.post('/register', function (req, res) {           
      var endpoint = req.body.endpoint;
      saveRegistrationDetails(endpoint, key, authSecret); 
      const pushSubscription = {                          
        endpoint: req.body.endpoint,
        keys: {
          auth: req.body.authSecret,
          p256dh: req.body.key
        }
      };
      var body = 'Thank you for registering';
      var iconUrl = 'https://example.com/images/homescreen.png';
      // 发送 Web 推送消息
      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('Web push app listening on port 3111!')
    });
    

    service worker监听push事件,将通知详情推送给用户

    service-worker.js

    self.addEventListener('push', function (event) {
     // 检查服务端是否发来了任何有效载荷数据
      var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
      var title = 'Progressive Times';
      event.waitUntil(
        // 使用提供的信息来显示 Web 推送通知
        self.registration.showNotification(title, {                           
          body: payload.msg,
          url: payload.url,
          icon: payload.icon
        })
      );
    });
    

    8、PWA小demo

    准备:

    创建一个关于PWA项目的文件夹

    文件夹内准备一张图

    一个index.html文件

    一个main.css文件

    一个manifest.json文件

    一个sw.js文件


    image

    css文件夹里有一个style.css

    images里有一个logo.jpg

    具体看代码:

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Hello PWA</title>
        <link rel="stylesheet" href="css/style.css">
    
    </head>
    <body>
        <img src="images/logo.jpg">
    
        <script>
            if ('serviceWorker' in navigator) { // 浏览器支持SW
                navigator.serviceWorker.register('sw.js').then(function (registration) {
                    console.log('ServiceWorker registration successful with scope: ', registration.scope);
                }).catch(function (err) {
                    console.log('ServiceWorker registration failed: ', err);
                });
            }
        </script>
    </body>
    </html>
    

    我的style.css是空的哈哈哈

    sw.js

    var cacheName = 'hello-pwa';
    
    self.addEventListener('install', event => {
        event.waitUntil(
            caches.open(cacheName)
              .then(cache => cache.addAll(
                [
                    '/',  // 这个一定要包含整个目录,不然无法离线浏览
                    './images/logo.jpg',
                    './index.html',
                    './css/style.css'
                ]
              )).then(() => self.skipWaiting())
        );
    });
    
    self.addEventListener('fetch', function (event) {  
        event.respondWith(
          caches.match(event.request)                    
          .then(function (response) {
            if (response) {                              
              return response;                           
            }
            return fetch(event.request);                 
          })
        );
    });
    

    接下来通过 http-server 和 ngrok(https)进行调试查看

    在当前文件下
    安装 http-server

    npm install http-server -g
    

    安装 ngrok,下载解压即可

    在项目目录下执行如下命令:

    http-server -c-1  // -c-1 会关闭缓存
    

    再开启另外一个终端在 ngrok 文件的目录下执行如下命令:

    ./ngrok http 8080 // http-server 默认开启8080端口
    
    运行:(我端口8080被占了,所以这里是8081) image

    查看application

    image

    查看缓存部分

    image

    第一次加载进来缓存没有东西,需要刷新一下页面。

    https://github.com/yangTwo100/PWA_search_demo

    以上。

    相关文章

      网友评论

          本文标题:H5 PWA技术

          本文链接:https://www.haomeiwen.com/subject/bcpsxrtx.html