探究 PWA 的实现与应用

2019-07-27 16:33:52 +08:00
 famanoder

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,但是他能拦截并修改访问的资源请求,通过多种缓存策略来对资源进行缓存和更新;

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);
  })
);

如果刷新页面后有新版的 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

wget https://dl.eff.org/certbot-auto
chmod a+x ./certbot-auto
./certbot-auto --help

这一步用来验证域名是否可访问,后面该工具会在配置的 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,在这里我们可以调用模态框组件,提醒用户是否刷新页面使用最新版;

2886 次点击
所在节点    前端开发
2 条回复
zhangkc
2019-07-27 18:26:13 +08:00
给个👍
julyclyde
2019-07-28 21:42:05 +08:00
散文啊,啥内容都有

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/586724

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX