V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
famanoder
V2EX  ›  前端开发

探究 PWA 的实现与应用

  •  
  •   famanoder ·
    famanoder · 2019-07-27 16:33:52 +08:00 · 3071 次点击
    这是一个创建于 1983 天前的主题,其中的信息可能已经有所发展或是发生改变。

    PWA:( Progressive web apps,渐进式 Web 应用)

    概念

    PWA 是现代 web 开发的一个新的理念,他不依赖于某个特定的 API,而是使用各种技术和模式来开发的 web 应用,并且同时具备 web 应用和原生应用的特性,以此来达到最佳 web 体验的目标;

    一个应用可以称为 PWA,应该具备以下特点:

    Discoverable 内容可以通过搜索引擎发现。

    Installable 可以出现在设备的主屏幕。

    Linkable 你可以简单地通过一个 URL 来分享它。

    Network independent 它可以在离线状态或者是在网速很差的情况下运行。

    Progressive 它在老版本的浏览器仍旧可以使用,在新版本的浏览器上可以使用全部功能。

    Re-engageable 无论何时有新的内容它都可以发送通知。

    Responsive 它在任何具有屏幕和浏览器的设备上可以正常使用——包括手机,平板电脑,笔记本,电视,冰箱,等。

    Safe 在你和应用之间的连接是安全的,可以阻止第三方访问你的敏感数据。

    所以,判断一个 web 应用是否是 PWA 需要看它是否同时具备原生应用的特性,比如:桌面图标,离线缓存,消息推送等;当然,他的好处也是很多的,比如:快!真的非常快,并且离线可访问;用户可以同意添加图标到主屏方便下次访问;还可以实现系统级的消息推送;总之,就是不断的接近原生应用的体验!

    技术实现

    实现一个 PWA 需要的核心技术包括:Service Worker + Manifest.json + HTTPS

    Service Worker

    Service Worker 是一个注册在指定资源和路径下的事件驱动 worker,因此它同其他类型 worker 一样不能访问 DOM,不允许使用同步的 API,比如 localStorage,但是他能拦截并修改访问的资源请求,通过多种缓存策略来对资源进行缓存和更新;

    • 注册一个 worker
    if ('serviceWorker' in navigator) {
      navigator
      .serviceWorker
      .register('/sw-test/sw.js', { 
        scope: '/sw-test/' 
      })
      .then(function(reg) {
        // registration worked
        console.log('Registration succeeded. Scope is ' + reg.scope);
      }).catch(function(error) {
        // registration failed
        console.log('Registration failed with ' + error);
      });
    }
    
    • 安装和激活:填充缓存

    sw 注册之后,浏览器会尝试安装并激活它,安装完成之后会触发 install 事件,为了达到离线缓存的目的,需要使用一个新的存储 API - caches,这个 APIsw 上的一个全局对象,他可以用来存储网络请求过来的资源,与浏览器标准存储不一样的是,他是特定你的域的持久化缓存;

    this.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open('v1').then(function(cache) {
          return cache.addAll([
            '/sw-test/app.js'
          ]);
        })
      );
    });
    
    • 控制请求的响应

    任何被 sw 控制的的被请求时,都会触发 fetch 事件,通过监听该事件可以控制请求的具体响应内容;

    如上,安装成功后可以将一批指定的资源缓存起来,那么现在就可以拦截请求,然后将匹配到的缓存结果作为响应,或者重新请求新版的资源,甚至可以响应指定的内容,你拦截了,那么你说了算!

    // 响应已缓存的请求
    
    this.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.match(event.request)
      );
    });
    
    // 响应自定义内容
    
    const res = new Response('<p>Hello, service worker!</p>', {
      headers: { 'Content-Type': 'text/html' }
    });
    event.respondWith(res);
    
    // 缓存获取失败重新请求最新的
    
    event.respondWith(
      caches.match(event.request).then(function(response) {
        return response || fetch(event.request);
      })
    );
    
    
    • 更新 Service Worker

    如果刷新页面后有新版的 sw,新版的会在后台安装,安装后并不会立即生效,当没有页面在使用旧的版本的 sw 时,新版的就会激活并响应请求;

    // 更新到 v2 版本
    
    this.addEventListener('install', function(event) {
      event.waitUntil(
        caches.open('v2').then(function(cache) {
          return cache.addAll([
            '/sw-test/app.js'
          ]);
        })
      );
    });
    
    • 删除旧缓存

    当有了新版本,旧版本还在运行的时候,为了避免缓存数据太多占满磁盘空间,需要对旧的缓存进行清理;通过监听 activate 事件,来对旧的缓存进行清理;

    self.addEventListener('activate', function(event) {
      var cacheWhitelist = ['v2'];
    
      event.waitUntil(
        caches.keys().then(function(keyList) {
          return Promise.all(keyList.map(function(key) {
            if (cacheWhitelist.indexOf(key) === -1) {
              return caches.delete(key);
            }
          }));
        })
      );
    });
    

    Manifest.json

    该文件里列出了将应用添加至桌面的所有配置信息,如果修改了该文件,已添加到桌面的应用样式不会改变,需要重新添加到桌面:

    <link rel="manifest" href="/manifest.json" />
    

    例如:

    {
      "name": "京东 PLUS 会员",
      "short_name": "京东 PLUS",
      "description": "随时随地分享新鲜事",
      "icons": [
        {
          "src": "https://img10.360buyimg.com/img/jfs/t1/65477/24/1867/249268/5d03380eE9c52b872/a7a2864e42dde553.gif",
          "sizes": "192x192",
          "type": "image/png"
        },
        {
          "src": "https://img10.360buyimg.com/img/jfs/t1/65477/24/1867/249268/5d03380eE9c52b872/a7a2864e42dde553.gif",
          "sizes": "512x512",
          "type": "image/png"
        }
      ],
      "share_target": {
          "action": "compose",
          "params": {
            "title": "title",
            "text": "text",
            "url": "url"
        }
      },
      "start_url": "/?standalone=1",
      "scope": "/",
      "display": "standalone",
      "orientation": "portrait",
      "background_color": "#F3F3F3",
      "theme_color": "#F3F3F3",
      "related_applications": [],
      "prefer_related_applications": false
    }
    

    HTTPS

    https 是在 http 的基础上对数据进行加密传输,涉及一次非对称加密与一次对称加密,即便传输过程中数据被劫持,只要私钥没有泄露,黑客也束手无策,所以,数据安全性非常高,HTTPS 数据传输过程

    letsencrypt 是一家为全球网站免费提供 https 证书的机构,并且支持泛域名,非常值得推荐使用,大公司有钱的请无视;

    certbotletsencrypt 官方提供的一个用来获取和更新 https 证书的工具,https://certbot.eff.org/ 在它的网站上可以根据自己的系统及服务情况,具体选择如何使用,下面以 CentOS 7Nginx 1.16.0 为例,看看如何免费为网站架上 https

    • 获取 certbot 客户端
    wget https://dl.eff.org/certbot-auto
    chmod a+x ./certbot-auto
    ./certbot-auto --help
    
    • 配置 nginx

    这一步用来验证域名是否可访问,后面该工具会在配置的 root 对应目录下创建临时的文件,如果不可以访问,将无权获取对应域名的证书;

    location ^~ /.well-known/acme-challenge/ {
       default_type "text/plain";
       root     /path/to/www;
    }
     
    location = /.well-known/acme-challenge/ {
       return 404;
    }
    

    别忘了重启 nginx

    sudo nginx -s reload
    
    • 生成证书
    ./certbot-auto certonly --webroot -w /path/to/www -d  famanoder.com -d www.famanoder.com 
    

    此处 -w 为上一步 nginx 里配置的 root-d 是需要获取证书的域名,-d 可以多次使用,也可以直接生成泛域名的证书,-d *.famanoder.com,良心之作啊,很多云服务上不仅收费贵,还不支持泛域名,一个域名花一笔钱,此处,大公司有钱的请无视!

    • 自动更新证书

    证书生成成功后,可以选择是否自动续签,因为每次生成的证书有效期为 3 个月;

    ./certbot-auto renew --dry-run
    

    通过这个命令可以测试上一步的自动续签是否可用;

    也可以手动更新证书:

    ./certbot-auto renew -v
    

    或者通过命令设置自动更新:

    ./certbot-auto renew --quiet --no-self-upgrade
    
    • 使用证书
    listen 443 ssl;
    server_name .famanoder.com;
    index index.html;
    root  /path/to/www;
     
    ssl_certificate      /etc/letsencrypt/live/famanoder.org/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/famanoder.org/privkey.pem;
    

    可另行查阅更多的如何在 nginx 上配置 https

    实战 PWA

    以上对 pwa 做了基本的介绍,以及阐述了一个 pwa 应用所需的基础设施,接下来使用 offline-plugin 在项目里实战 pwa

    首先,在我们应用的入口文件中将其引入,保证当前页面与 service worker 能够通信;

    import(/* webpackChunkName: "offline" */'offline-plugin/runtime')
    .then(offline => {
      offline.install({
        onUpdating: () => {
          console.log('SW Event:', 'onUpdating');
        },
        onUpdateReady: () => {
          console.log('SW Event:', 'onUpdateReady');
          offline.applyUpdate();
        },
        onUpdated: () => {
          console.log('SW Event:', 'onUpdated');
          // window.location.reload();
          // alert('有新版可用,是否刷新?');
        },
        onUpdateFailed: () => {
          console.log('SW Event:', 'onUpdateFailed');
        }
      });
    });
    

    该插件会根据 webpack 打包生成的文件,生成 sw.js 文件,配置 webpack

    // webpack.config.js
    
    const OfflinePlugin = require('offline-plugin');
    
    module.exports = {
      // ...,
      plugins: [
        // ...,
        new OfflinePlugin({
          ServiceWorker: {
              events: true
          }
        })
      ]
    }
    

    这样,我们的应用就能够支持 pwa 了,可在控制台查看相关信息;

    使用 pwa 还有非常重要的一点,就是如何更新,如上所述,sw 在安装新版本后并不会立即激活,大多数时候都需要用户再一次刷新页面才会生效,当然,这个还跟具体的缓存策略有关,目前,观察一些 pwa 网站,会发现当有新版更新后,网站会通过一个模态框来提醒用户是否立即刷新页面使用最新版本,该插件的 runtime 里提供了 onUpdated 钩子,当最新版安装完成后会通知页面触发 onUpdated,在这里我们可以调用模态框组件,提醒用户是否刷新页面使用最新版;

    2 条回复    2019-07-28 21:42:05 +08:00
    zhangkc
        1
    zhangkc  
       2019-07-27 18:26:13 +08:00 via iPhone
    给个👍
    julyclyde
        2
    julyclyde  
       2019-07-28 21:42:05 +08:00
    散文啊,啥内容都有
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1017 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 22:04 · PVG 06:04 · LAX 14:04 · JFK 17:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.