上篇文章我们对渐进式 web 应用 (PWA) 做了一些基本的介绍.
渐进式 Web 应用 (PWA) 入门教程(上)
在这一节中, 我们将介绍 PWA 的原理是什么, 它是如何开始工作的.
第一步: 使用 HTTPS
渐进式 Web 应用程序需要使用 HTTPS 连接. 虽然使用 HTTPS 会让您服务器的开销变多, 但使用 HTTPS 可以让您的网站变得更安全, HTTPS 网站在 Google 上的排名也会更靠前.
由于 Chrome 浏览器会默认将 localhost 以及 127.x.x.x 地址视为测试地址, 所以在本示例中您并不需要开启 HTTPS. 另外, 出于调试目的, 您可以在启动 Chrome 浏览器的时候使用以下参数来关闭其对网站 HTTPS 的检查:
- --user-data-dir
- --unsafety-treat-insecure-origin-as-secure
第二步: 创建一个应用程序清单(Manifest)
应用程序清单提供了和当前渐进式 Web 应用的相关信息, 如:
应用程序名
描述
所有图片(包括主屏幕图标, 启动屏幕页面和用的图片或者网页上用的图片)
本质上讲, 程序清单是页面上用到的图标和主题等资源的元数据.
程序清单是一个位于您应用根目录的 JSON 文件. 该 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"
- }
- ]
- }
程序清单文件建立完之后, 你需要在每个页面上引用该文件:
<link rel="manifest" href="/manifest.json">
以下属性在程序清单中经常使用, 介绍说明如下:
name: 用户看到的应用名称
short_name: 应用短名称. 当显示应用名称的地方不够时, 将使用该名称.
description: 应用描述.
start_url: 应用起始路径, 相对路径, 默认为 /.
scope: URL 范围. 比如: 如果您将 "/app/" 设置为 URL 范围时, 这个应用就会一直在这个目录中.
background_color: 欢迎页面的背景颜色和浏览器的背景颜色(可选)
theme_color: 应用的主题颜色, 一般都会和背景颜色一样. 这个设置决定了应用如何显示.
orientation: 优先旋转方向, 可选的值有: any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, and portrait-secondary
display: 显示方式 --fullscreen(无 Chrome),standalone(和原生应用一样),minimal-ui(最小的一套 UI 控件集)或者 browser(最古老的使用浏览器标签显示)
icons: 一个包含所有图片的数组. 该数组中每个元素包含了图片的 URL, 大小和类型.
第三步: 创建一个 Service Worker
Service Worker 是一个可编程的服务器代理, 它可以拦截或者响应网络请求. Service Worker 是位于应用程序根目录的一个个的 JavaScript 文件.
您需要在页面对应的 JavaScript 文件中注册该 ServiceWorker:
- if ('serviceWorker' in navigator) {
- // register service worker
- navigator.serviceWorker.register('/service-worker.js');
- }
如果您不需要离线的相关功能, 您可以只创建一个 /service-worker.js 文件, 这样用户就可以直接安装您的 Web 应用了!
Service Worker 这个概念可能比较难懂, 它其实是一个工作在其他线程中的标准的 Worker, 它不可以访问页面上的 DOM 元素, 没有页面上的 API, 但是可以拦截所有页面上的网络请求, 包括页面导航, 请求资源, Ajax 请求.
上面就是使用全站 HTTPS 的主要原因了. 假设您没有在您的网站中使用 HTTPS, 一个第三方的脚本就可以从其他的域名注入他自己的 ServiceWorker, 然后篡改所有的请求 -- 这无疑是非常危险的.
Service Worker 会响应三个事件: install,activate 和 fetch.
Install 事件
该事件将在应用安装完成后触发. 我们一般在这里使用 Cache API https://developer.mozilla.org/en-US/docs/Web/API/Cache 缓存一些必要的文件.
首先, 我们需要提供如下配置
缓存名称 (CACHE) 以及版本(version). 应用可以有多个缓存存储, 但是在使用时只会使用其中一个缓存存储. 每当缓存存储有变化时, 新的版本号将会指定到缓存存储中. 新的缓存存储将会作为当前的缓存存储, 之前的缓存存储将会被作废.
一个离线的页面地址(offlineURL): 当用户访问了之前没有访问过的地址时, 该页面将会显示.
一个包含了所有必须文件的数组, 包括保障页面正常功能的 CSS 和 JavaScript. 在本示例中, 我还添加了主页和 logo. 当有不同的 URL 指向同一个资源时, 你也可以将这些 URL 分别写到这个数组中. offlineURL 将会加入到这个数组中.
我们也可以将一些非必要的缓存文件(installFilesDesirable). 这些文件在安装过程中将会被下载, 但如果下载失败, 不会触发安装失败.
- // 配置文件
- 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 https://developer.mozilla.org/en-US/docs/Web/API/Cache 将文件存储到缓存中.
- // 安装静态资源
- function installStaticFiles() {
- return caches.open(CACHE)
- .then(cache => {
- // 缓存可选文件
- cache.addAll(installFilesDesirable);
- // 缓存必须文件
- return cache.addAll(installFilesEssential);
- });
- }
最后, 我们添加一个 install 的事件监听器. waitUntil 方法保证了 service worker 不会安装直到其相关的代码被执行. 这里它会执行
installStaticFiles()
方法, 然后 self.skipWaiting()方法来激活 service worker:
- // 应用安装
- self.addEventListener('install', event => {
- console.log('service worker: install');
- // 缓存主要文件
- event.waitUntil(
- installStaticFiles()
- .then(() => self.skipWaiting())
- );
- });
Activate 事件
这个事件会在 service worker 被激活时发生. 你可能不需要这个事件, 但是在示例代码中, 我们在该事件发生时将老的缓存全部清理掉了:
- // 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()
执行时将会把当前 service worker 作为被激活的 worker.
Fetch 事件 该事件将会在网络开始请求时发起. 该事件处理函数中, 我们可以使用 respondWith()方法来劫持 HTTP 的 GET 请求然后返回:
从缓存中取到的资源文件
如果第一步失败, 资源文件将会从网络中使用 Fetch API 来获取(和 service worker 中的 fetch 事件无关). 获取到的资源将会加入到缓存中.
如果第一步和第二步均失败, 将会从缓存中返回正确的资源文件.
- // 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)方法中使用了一些 helper 方法来返回正确的数据:
- // 是否为图片地址?
- 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 返回离线资源
- function offlineAsset(url) {
- if (isImage(url)) {
- // 返回图片
- 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 页面.
Chrome 开发者工具中的 ServiceWorker 部分提供了关于当前页面 worker 的信息. 其中会显示 worker 中发生的错误, 还可以强制刷新, 也可以让浏览器进入离线模式.
Cache Storage 部分例举了当前所有已经缓存的资源. 你可以在缓存需要更新的时候点击 refresh 按钮.
第四步: 创建可用的离线页面
离线页面可以是静态的 html, 一般用于提醒用户当前请求的页面暂时无法使用. 然而, 我们可以提供一些可以阅读的页面链接.
Cache API 可以在 main.js 中使用. 然而, 该 API 使用 Promise, 在不支持 Promise 的浏览器中会失败, 所有的 JavaScript 执行会因此受到影响. 为了避免这种情况, 在访问 / js/offlinepage.js 的时候我们添加了一段代码来检查当前是否在离线环境中:
/js/offlinepage.js 中以版本号为名称保存了最近的缓存, 获取所有 URL, 删除不是页面的 URL, 将这些 URL 排序然后将所有缓存的 URL 展示在页面上:
- // 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);
- });
- })
- });
开发者工具
Chrome 浏览器提供了一系列的工具来帮助您来调试 Service Worker, 日志也会直接显示在控制台上.
您最好使用匿名模式来进行开发工作, 这样可以排除缓存对开发的干扰.
最后, Chrome 的 Lighthouse https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk 扩展也可以为您的渐进式 Web 应用提供一些改进信息.
渐进式 Web 应用的要点
渐进式 Web 应用是一种新的技术, 所以使用的时候一定要小心. 也就是说, 渐进式 Web 应用可以让您的网站在几个小时内得到改善, 并且在不支持渐进式 Web 应用的浏览器上也不会影响网站的显示.
但是我们需要考虑以下几点:
URL 隐藏
当您的应用就是一个单 URL 的应用程序时(比如游戏), 我建议您隐藏地址栏. 除此之外的情况我并不建议您隐藏地址栏. 在 Manifest 中, display: minimal-ui 或者 display: browser 对于大多数情况来说足够用了.
缓存过大
你不能将您网站中的所有内容缓存下来. 对于小一些的网站来说缓存所有内容并不是一个问题, 但是如果一个网站包含了上千个页面呢? 很明显不是所有人对网站中的所有内容都感兴趣. 存储是有限制的, 如果您将所有访问过的页面都缓存下来的话, 缓存大小会增长额很快.
你可以这样制定你的缓存策略:
只缓存重要的页面, 比如主页, 联系人页面和最近浏览文章的页面.
不要缓存任何图片, 视频和大文件
定时清理旧的缓存
提供一个 "离线阅读" 按钮, 这样用户就可以选择需要缓存哪些内容了.
缓存刷新
示例代码中在发起请求之前会先查询缓存. 当用户处于离线状态时, 这很好, 但是如果用户处于在线状态, 那他只会浏览到比较老旧的页面.
各种资源比如图片和视频不会改变, 所以一般都把这些静态资源设置为长期缓存. 这些资源可以直接缓存一年(31,536,000 秒). 在 HTTP Header 中, 就是:
Cache-Control: max-age=31536000
页面, CSS 和脚本文件可能变化的更频繁一些, 所以你可以设置一个比较小的缓存超时时间(24 小时), 并确保在用户网络连接恢复时再次从服务器请求:
Cache-Control: must-revalidate, max-age=86400
你也可以在每次网站发布时, 通过改名的方式强制浏览器重新请求资源.
小结
至此, 相信你如果按照本文一步一步操作下来, 你也可以很快把自己的 Web 应用转为 PWA. 在转为了 PWA 后, 如果有使用满足 PWA 模型的前端控件的需求, 你可以试试纯前端表格控件 SpreadJS http://www.grapecity.com.cn/developer/spreadjs , 适用于 .NET,Java 和移动端等平台的表格控件一定不会令你失望的.
来源: https://www.cnblogs.com/powertoolsteam/p/ProgressiveWebApps_2.html