如何把网站改成PWA

如何把网站改成PWA

游戏|数码彩彩2024-06-03 7:39:33332A+A-

如何把网站改成PWA,首先我们要了解知道什么是PWA

1. PWA是什么

Progressive Web Apps (下文以“PWAs”代指) 是一个令人兴奋的前端技术的革新。PWAs综合了一系列技术使你的 web app表现得就像是 native mobile app。相比于纯 web 解决方案和纯 native 解决方案,PWAs对于开发者和用户有以下优点:

  • 你只需要基于开放的 W3C 标准的 web 开发技术来开发一个app。不需要多客户端开发。
  • 用户可以在安装前就体验你的 app。
  • 不需要通过 AppStore 下载 app。app 会自动升级不需要用户升级。
  • 用户会受到‘安装’的提示,点击安装会增加一个图标到用户首屏。
  • 被打开时,PWA 会展示一个有吸引力的闪屏。
  • chrome 提供了可选选项,可以使 PWA 得到全屏体验。
  • 必要的文件会被本地缓存,因此会比标准的web app 响应更快(也许也会比native app响应快)
  • 安装及其轻量 -- 或许会有几百 kb 的缓存数据。
  • 网站的数据传输必须是 https 连接。
  • PWAs 可以离线工作,并且在网络恢复时可以同步最新数据。

虽然PWA不是所有浏览器都支持,但是我们不需要担心, 因为pwa是渐进增强的, 你的app仍然可以运行在不支持 PWA 技术的浏览器里。用户不能离线访问,不过其他功能都像原来一样没有影响。综合利弊得失,没有理由不把你的 app 改进为 PWA。

2. 步骤

将你的网站改进为一个 Progressive Web App 总共有三个必要步骤:

2.1 第一步:开启 HTTPS

由于一些显而易见的原因,PWAs 需要 HTTPS 连接。HTTPS 在示例代码中并不是必须的,因为 Chrome 允许使用 localhost 或者任何 127.x.x.x 的地址来测试。你也可以在 HTTP 连接下测试你的 PWA,你需要使用 Chrome ,并且输入以下命令行参数:

  • --user-data-dir
  • --unsafety-treat-insecure-origin-as-secure

2.2 第二步:创建一个 Web App Manifest

manifest 文件提供了一些我们网站的信息,例如 name,description 和需要在主屏使用的图标的图片,启动屏的图片等。

manifest文件是一个 JSON 格式的文件,位于你项目的根目录。它必须用Content-Type: application/manifest+json 或者 Content-Type: application/json这样的 HTTP 头来请求。这个文件可以被命名为任何名字,在示例代码中他被命名为 /manifest.json:

{
  "name"              : "PWA Website",
  "short_name"        : "PWA",
  "description"       : "An example PWA website",
  "start_url"         : "/",
  "display"           : "standalone",
  "orientation"       : "any",
  "background_color"  : "#ACE",
  "theme_color"       : "#ACE",
  "icons": [
    {
      "src"           : "/images/logo/logo072.png",
      "sizes"         : "72x72",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo152.png",
      "sizes"         : "152x152",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo192.png",
      "sizes"         : "192x192",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo256.png",
      "sizes"         : "256x256",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo512.png",
      "sizes"         : "512x512",
      "type"          : "image/png"
    }
  ]
}

在页面的<head>中引入:

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

manifest 中主要属性有:

  • name —— 网页显示给用户的完整名称
  • short_name —— 当空间不足以显示全名时的网站缩写名称
  • description —— 关于网站的详细描述
  • start_url —— 网页的初始 相对 URL(比如 /)
  • scope —— 导航范围。比如,/app/的scope就限制 app 在这个文件夹里。
  • background-color —— 启动屏和浏览器的背景颜色
  • theme_color —— 网站的主题颜色,一般都与背景颜色相同,它可以影响网站的显示
  • orientation —— 首选的显示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary。
  • display —— 首选的显示方式:fullscreen, standalone(看起来像是native app),minimal-ui(有简化的浏览器控制选项) 和 browser(常规的浏览器 tab)
  • icons —— 定义了 src URL, sizes和type的图片对象数组。

MDN提供了完整的manifest属性列表:Web App Manifest properties

在开发者工具中的 Application tab 左边有 Manifest 选项,你可以验证你的 manifest JSON 文件,并提供了 “Add to homescreen”。

如何把网站改成PWA

 


2020042301.png

 

2.3 第三步:创建一个 Service Worker

Service Worker 是拦截和响应你的网络请求的编程接口。这是一个位于你根目录的一个单独的 JAVAscript 文件。

你的 js 文件(在示例代码中是 /js/main.js)可以检查是否支持 Service Worker,并且注册:

if ('serviceWorker' in navigator) {

  // register service worker
  navigator.serviceWorker.register('/service-worker.js');

}

如果你不需要离线功能,可以简单的创建一个空的 /service-worker.js文件 —— 用户会被提示安装你的 app。

Service Worker 很复杂,你可以修改示例代码来达到自己的目的。这是一个标准的 web worker,浏览器用一个单独的线程来下载和执行它。它没有调用 DOM 和其他页面 api 的能力,但他可以拦截网络请求,包括页面切换,静态资源下载,ajax请求所引起的网络请求。

这就是需要 HTTPS 的最主要的原因。想象一下第三方代码可以拦截来自其他网站的 service worker, 将是一个灾难。

service worker 主要有三个事件: install,activate 和 fetch。

Install 事件

这个事件在app被安装时触发。它经常用来缓存必要的文件。缓存通过 Cache API来实现。

首先,我们来构造几个变量:

  1. 缓存名称(CACHE)和版本号(version)。你的应用可以有多个缓存但是只能引用一个。我们设置了版本号,这样当我们有重大更新时,我们可以更新缓存,而忽略旧的缓存。
  2. 一个离线页面的URL(offlineURL)。当离线时用户试图访问之前未缓存的页面时,这个页面会呈现给用户。
  3. 一个拥有离线功能的页面必要文件的数组(installFilesEssential)。这个数组应该包含静态资源,比如 css 和 JavaScript 文件,但我也把主页面(/)和图标文件写进去了。如果主页面可以多个URL访问,你应该把他们都写进去,比如/和/index.html。注意,offlineURL也要被写入这个数组。
  4. 可选的,描述文件数组(installFilesDesirable)。这些文件都很会被下载,但如果下载失败不会中止安装。
// configuration
const
  version = '1.0.0',
  CACHE = version + '::PWAsite',
  offlineURL = '/offline/',
  installFilesEssential = [
    '/',
    '/manifest.json',
    '/css/styles.css',
    '/js/main.js',
    '/js/offlinepage.js',
    '/images/logo/logo152.png'
  ].concat(offlineURL),
  installFilesDesirable = [
    '/favicon.ico',
    '/images/logo/logo016.png',
    '/images/hero/power-pv.jpg',
    '/images/hero/power-lo.jpg',
    '/images/hero/power-hi.jpg'
  ];

installStaticFiles()方法添加文件到缓存,这个方法用到了基于 promise的 Cache API。当必要的文件都被缓存后才会生成返回值。

// install static assets
function installStaticFiles() {

  return caches.open(CACHE)
    .then(cache => {

      // cache desirable files
      cache.addAll(installFilesDesirable);

      // cache essential files
      return cache.addAll(installFilesEssential);

    });

}

最后,我们添加install的事件监听函数。 waitUntil方法确保所有代码执行完毕后,service worker 才会执行 install。执行 installStaticFiles()方法,然后执行 self.skipWaiting()方法使service worker进入 active状态。

// application installation
self.addEventListener('install', event => {

  console.log('service worker: install');

  // cache core files
  event.waitUntil(
    installStaticFiles()
    .then(() => self.skipWaiting())
  );

});

Activate 事件

当 install完成后, service worker 进入active状态,这个事件立刻执行。你可能不需要实现这个事件监听,但是示例代码在这里删除老旧的无用缓存文件:

// clear old caches
function clearOldCaches() {

  return caches.keys()
    .then(keylist => {

      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );

    });

}

// application activated
self.addEventListener('activate', event => {

  console.log('service worker: activate');

    // delete old caches
  event.waitUntil(
    clearOldCaches()
    .then(() => self.clients.claim())
    );

});

注意,最后的self.clients.claim()方法设置本身为active的service worker。

Fetch 事件

当有网络请求时这个事件被触发。它调用respondWith()方法来劫持 GET 请求并返回:

  1. 缓存中的一个静态资源。
  2. 如果 #1 失败了,就用 Fetch API(这与 service worker 的fetch 事件没关系)去网络请求这个资源。然后将这个资源加入缓存。
  3. 如果 #1 和 #2 都失败了,那就返回一个适当的值。
// application fetch network data
self.addEventListener('fetch', event => {

  // abandon non-GET requests
  if (event.request.method !== 'GET') return;

  let url = event.request.url;

  event.respondWith(

    caches.open(CACHE)
      .then(cache => {

        return cache.match(event.request)
          .then(response => {

            if (response) {
              // return cached file
              console.log('cache fetch: ' + url);
              return response;
            }

            // make network request
            return fetch(event.request)
              .then(newreq => {

                console.log('network fetch: ' + url);
                if (newreq.ok) cache.put(event.request, newreq.clone());
                return newreq;

              })
              // app is offline
              .catch(() => offlineAsset(url));

          });

      })

  );

});

最后这个offlineAsset(url)方法通过几个辅助函数返回一个适当的值:

// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {

  return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);

}


// return offline asset
function offlineAsset(url) {

  if (isImage(url)) {

    // return image
    return new Response(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers: {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'no-store'
      }}
    );

  }
  else {

    // return page
    return caches.match(offlineURL);

  }

}

offlineAsset()方法检查是否是一个图片请求,如果是,那么返回一个带有 “offline” 字样的 SVG。如果不是,返回 offlineURL 页面。

开发者工具提供了查看 Service Worker 相关信息的选项:

 

如何把网站改成PWA

 


2020042302.png

在开发者工具的 Cache Storage 选项列出了所有当前域内的缓存和所包含的静态文件。当缓存更新的时候,你可以点击左下角的刷新按钮来更新缓存:

 

如何把网站改成PWA

 


2020042303.png

 

不出意料, Clear storage 选项可以删除你的 service worker 和缓存:

 

如何把网站改成PWA

 


2020042304.png

2.4 第四步:创建一个可用的离线页面

离线页面可以是一个静态页面,来说明当前用户请求不可用。然而,我们也可以在这个页面上列出可以访问的页面链接。

在main.js中我们可以使用 Cache API 。然而API 使用promises,在不支持的浏览器中会引起所有javascript运行阻塞。为了避免这种情况,我们在加载另一个 /js/offlinepage.js 文件之前必须检查离线文件列表和是否支持 Cache API 。

// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
  var scr = document.createElement('script');
  scr.src = '/js/offlinepage.js';
  scr.async = 1;
  document.head.appendChild(scr);
}

/js/offlinepage.js locates the most recent cache by version name, 取到所有 URL的key的列表,移除所有无用 URL,排序所有的列表并且把他们加到 ID 为cachedpagelist的 DOM 节点中:

// cache name
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');

// fetch all caches
window.caches.keys()
  .then(cacheList => {

    // find caches by and order by most recent
    cacheList = cacheList
      .filter(cName => cName.includes(CACHE))
      .sort((a, b) => a - b);

    // open first cache
    caches.open(cacheList[0])
      .then(cache => {

        // fetch cached pages
        cache.keys()
          .then(reqList => {

            let frag = document.createDocumentFragment();

            reqList
              .map(req => req.url)
              .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
              .sort()
              .forEach(req => {
                let
                  li = document.createElement('li'),
                  a = li.appendChild(document.createElement('a'));
                  a.setAttribute('href', req);
                  a.textContent = a.pathname;
                  frag.appendChild(li);
              });

            if (list) list.appendChild(frag);

          });

      })

  });

上面这些代码demo在 https://github.com/craigbuckler/pwa-retrofit

上面只简单讲了 Service Worker 如何工作。我们会发现有很多问题需要我们进一步解决:

  • 预缓存的静态资源修改后在下一次发版本时的文件名都不一样,手动写死太低效,最好每次都自动生成资源文件名。
  • 缓存资源是以硬编码字符串判断是否有效,这样每次发版本都需要手动修改,才能更新缓存。并且每次都是全量更新。能否以文件的粒度进行资源缓存呢?
  • 请求代理没有区分静态资源和动态接口。已经缓存的动态接口也会一直返回缓存,无法请求新数据。

上面只列出了三个明显的问题,还有很多问题是没有考虑到的。如果让我们自己来解决这些问题,不仅是工作量很大,而且也很难写出生产环境可用的 Service Worker。

3. workbox

既然如此,我们最好是站在巨人的肩膀上,这个巨人就是谷歌。workbox 是由谷歌浏览器团队发布,用来协助创建 PWA 应用的 JavaScript 库。当然直接用 workbox 还是太复杂了,谷歌还很贴心的发布了一个 webpack 插件,能够自动生成 Service Worker 和 静态资源列表 - workbox-webpack-plugin。

只需简单一步就能生成生产环境可用的 Service Worker :

const { GenerateSW } = require('workbox-webpack-plugin')

new GenerateSW()
点击这里复制本文地址 版权声明:本文内容由网友提供,该文观点仅代表作者本人。本站(https://www.angyang.net.cn)仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。

昂扬百科 © All Rights Reserved.  渝ICP备2023000803号-3网赚杂谈