渐进式 Web
应用首先是一种应用,它根据设备的支持情况来提供更多功能,提供离线能力,推送通知,甚至原生应用的外观和速度,以及对资源进行本地缓存。
渐进式 Web
应用是一个网站,它使用了某些开发技术,使其体验比普通针对移动优化的网站体验更好。它使用起来就像是在使用一个原生应用一样
渐进式 Web
应用可能是一个不清晰的术语,而更好的定义是:它们是一种 Web
应用,利用现代浏览器特性(比如 Web Worker
和 Web
应用清单),让移动设备对其“升级”,使之成为一等公民角色的应用程序。
PWA 结合了最好的 Web
应用和最好的原生应用的用户体验。包含以下:
Service Workers
允许离线工作,或在低质量网络上工作。service Woker
的更新进程,应用能始终保持最新状态。HTTPS
,防止窥探,并确保内容没有被篡改W3C
清单和 service Worker
注册作用域,搜索引擎可找到它们,可以识别为“应用程序”。URL
轻松共享,不需要复杂的安装。渐进式 Web
应用的定义中有部分是这样说的:它必须支持离线工作。
由于允许 Web
应用程序脱机工作的是 Service Worker
,这意味着 Service Worker
是渐进式 Web
应用强制要求的部分。
渐进式 Web
应用程序需要使用 HTTPS 连接。虽然使用 HTTPS
会让您服务器的开销变多,但使用 HTTPS
可以让您的网站变得更安全 ,如何给网站开启 https
Manifest
)应用程序清单提供了和当前渐进式 Web
应用的相关信息,如:
本质上讲,程序清单是页面上用到的图标和主题等资源的元数据。
程序清单是一个位于您应用根目录的 JSON
文件。该 JSON
文件返回时必须添加 Content-Type: application/manifest+json
或者 Content-Type: application/jsonHTTP
头信息。程序清单的文件名不限,在本文的示例代码中为 manifest.json
:
// manifest.json
{
"dir": "ltr",
"lang": "en",
"name": "D.D Blog",
"scope": "/",
"display": "standalone",
"start_url": "/",
"short_name": "D.D Blog",
"theme_color": "transparent",
"description": "Share More, Gain More. - D.D Blog",
"orientation": "any",
"background_color": "transparent",
"related_applications": [],
"prefer_related_applications": false,
"icons": [
{
"src": "assets/img/logo/size-32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "assets/img/logo/size-48.png",
"sizes": "48x48",
"type": "image/png"
} //...
],
"gcm_sender_id": "...",
"applicationServerKey": "..."
}
程序清单文件建立完之后,你需要在每个页面上引用该文件:
<link rel="manifest" href="/manifest.json" />
以下属性在程序清单中经常使用,介绍说明如下:
short_name
: 应用展示的名字icons
: 定义不同尺寸的应用图标start_url
: 定义桌面启动的 URL
description
: 应用描述,可以参考 meta
中的 description
display
: 定义应用的显示方式,有 4 种显示方式,分别为:fullscreen
: 全屏standalone
: 应用minimal-ui
: 类似于应用模式,但比应用模式多一些系统导航控制元素,但又不同于浏览器模式browser
: 浏览器模式,默认值name
: 应用名称orientation
: 定义默认应用显示方向,竖屏、横屏prefer_related_applications
: 是否设置对应移动应用,默认为 falserelated_applications
: 获取移动应用的方式background_color
: 应用加载之前的背景色,用于应用启动时的过渡theme_color
: 定义应用默认的主题色dir
: 文字方向,3 个值可选 ltr(left-to-right)
, rtl(right-to-left)
和 auto
(浏览器判断),默认为 auto
lang
: 语言scope
: 定义应用模式下的路径范围,超出范围会已浏览器方式显示manifest 注意事项
5M
manifest
文件,或者内部列举的某一个文件不能正常下载,整个更新过程将视为失败,浏览器继续全部使用老的缓存manifest
的 html 必须与 manifest
文件同源,在同一个域下manifest
中使用的相对路径,相对参照物为 manifest
文件CACHE MANIFEST
字符串应在第一行,且必不可少HTML
文件manifest
文件中 CACHE
则与 NETWORK,FALLBACK
的位置顺序没有关系,如果是隐式声明需要在最前面FALLBACK
中的资源必须和 manifest
文件同源manifest
属性,请求的资源如果在缓存中也从缓存中访问manifest
文件发生改变时,资源请求本身也会触发更新Service Worker
是一个可编程的服务器代理,它可以拦截或者响应网络请求。ServiceWorker
是位于应用程序根目录的一个个的 JavaScript
文件。 您需要在页面对应的 JavaScript
文件中注册该 ServiceWorker:
//main.js
if ("serviceWorker" in navigator) {
// 注册 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
。
Service Worker
和Web Worker
不是同一个东西 ,不要搞混淆了
该事件将在应用安装完成后触发。我们一般在这里使用 CacheAPI
缓存一些必要的文件。
首先,我们需要提供如下配置
CACHE
)以及版本(version
)。应用可以有多个缓存存储,但是在使用时只会使用其中一个缓存存储。每当缓存存储有变化时,新的版本号将会指定到缓存存储中。新的缓存存储将会作为当前的缓存存储,之前的缓存存储将会被作废。offlineURL
):当用户访问了之前没有访问过的地址时,该页面将会显示。CSS
和 JavaScript
。在本示例中,我还添加了主页和 logo
。当有不同的 URL 指向同一个资源时,你也可以将这些 URL 分别写到这个数组中。offlineURL
将会加入到这个数组中。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
的方式使用 CacheAPI
将文件存储到缓存中。
// 安装 静态资源
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()));
});
这个事件会在 service worker
被激活时发生。你可能不需要这个事件,但是在示例代码中,我们在该事件发生时将老的缓存全部清理掉了:
// 清理旧的缓存
function clearOldCaches() {
return caches.keys().then((keylist) => {
return Promise.all(
keylist.filter((key) => key !== CACHE).map((key) => caches.delete(key))
);
});
}
// 程序激活
self.addEventListener("activate", (event) => {
console.log("service worker: activate");
// 删除旧的缓存
event.waitUntil(clearOldCaches().then(() => self.clients.claim()));
});
注意
self.clients.claim()
执行时将会把当前service worker
作为被激活的 worker。
Fetch
事件该事件将会在网络开始请求时发起。该事件处理函数中,我们可以使用 respondWith()
方法来劫持 HTTP
的 GET
请求然后返回:
Fetch API
来获取(和 service worker
中的 fetch
事件无关)。获取到的资源将会加入到缓存中。// 程序获取网络数据
self.addEventListener("fetch", (event) => {
// 放弃非get请求
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) {
// 还回缓存文件
console.log("cache fetch: " + url);
return response;
}
// 发起网络请求
return (
fetch(event.request)
.then((newreq) => {
console.log("network fetch: " + url);
if (newreq.ok) cache.put(event.request, newreq.clone());
return newreq;
})
// 程序离线
.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);
}
// 返回离线资源
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 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
的时候我们添加了一段代码来检查当前是否在离线环境中:
// 加载脚本以填充脱机页列表
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
中以版本号为名称保存了最近的缓存,获取所有 URL
,删除不是页面的 URL
,将这些 URL
排序然后将所有缓存的 URL
展示在页面上:
// 缓存名称
const CACHE = "::PWAsite",
offlineURL = "/offline/",
list = document.getElementById("cachedpagelist");
// 获取所有缓存
window.caches.keys().then((cacheList) => {
// 按最近的查找缓存和排序
cacheList = cacheList
.filter((cName) => cName.includes(CACHE))
.sort((a, b) => a - b);
// 打开第一个
caches.open(cacheList[0]).then((cache) => {
// 获取已经缓存的页面
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);
});
});
});
ServiceWorker
在 SPA
中的应用根据《深入浅出 Webpack》 只需要安装 serviceworker-webpack-plugin 组件
//webpack config
import ServiceWorkerWebpackPlugin from 'serviceworker-webpack-plugin';
plugins: [
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, 'src/sw.js'),
}),
],
//mian.js
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
if ('serviceWorker' in navigator) {
const registration = runtime.register();
}
//sw.js
{
assets: [
'./main.256334452761ef349e91.js',
....
],
}
事实上能构建除想要的结果也能达到,离线缓存. 但是离线缓存文件除了图片等静态变的资源外, 每次打包构建的
hash
他也会随之改变, 不可能每次都手动修改静态文件资源列表. 于是:
推荐使用sw-precache-webpack-plugin
//webpack config
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
plugins: [
new SWPrecacheWebpackPlugin({
cacheId: "appName",
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: "service-worker.js",
minify: true,
navigateFallback: "/index.html",
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
}),
];
//main.js
if ("serviceWorker" in navigator && process.env.NODE_ENV == "production") {
navigator.serviceWorker.register("/service-worker.js");
}
每次编译代码之后会自动生成静态资源列表.
可以打开浏览器的调试器 Application
-> Service Workers
看到 服务已经启动
在 Application -> Cache -> Cache Storage 里面可以看到缓存的静态文件
Service Worker
本质上提供了类似 · 的功能,其作为 Web Application
以及 Server
之间的代理服务器,可以截获用户的请求。但是为了实现离线缓存功能,还需要结合 Cache API
。
使用 Cache Storage
还需要注意以下几点:
GET
请求;CDN
,不过无法对跨域请求的请求头和内容进行修改Cache Storage
的大小有硬性的限制,所以需要清理不必要的缓存。在切换到 Network
-> all
就可以看到被缓存的文件的 Size
那栏 (from ServiceWorker
不同于 from disk cache
)
为了验证网页在离线时能访问的能力,需要在开发者工具中的 Network
一栏中通过 Offline
选项禁用掉网络,再刷新页面能正常访问,并且网络请求的响应都来自 Service Workers
,正常的效果如图:
使用分享功能,需要满足以下几点:
HTTPS
token
加入页面 meta
中text
或 url
中的一项,且值必须为字符串// CommonService.js
export const isSupportShareAPI = () => !!navigator.share;
export const sharePage = () => {
navigator
.share({
title: document.title,
text: document.title,
url: window.location.href,
})
.then(() => console.info("Successful share."))
.catch((error) => console.log("Error sharing:", error));
};
PWA 消息推送(传送门)