谷歌开发者大会2017心得:PWA 渐进式网页应用
Takeaways from Google Developer Days 2017: PWA Progressive Web App
这是Google Developer Days 2017 Take Ways系列的第二篇。在今年的GDG上,渐进式网页应用(PWA,Progressive Web App)是一个反复被提起的技术,我至少参与两次主题是 PWA 的演讲。
PWA所要解决的主要问题是能让 Web App 有着接近 Native App 的效率。另外,在人们不太乐意下载新App的情况下,PWA 给开发者的应用提供一个入口,用户可以通过搜索引擎进入PWA。此外,PWA是可以像 Native App一样被用户保存到桌面,这个过程很轻松就像在手机浏览器里保存书签一样。
关于 PWA 的基本概念
Manifest.json
简而言之,Web Application Manifest 就是Web App的一张名片。它是一个JSON文件,开发者可以在这个JSON文件中声明他的Web App的一些元信息,比如:
- name
- description
- theme_color
- start_url
下面是一个 manifest.json 的例子:
{
"background_color": "white",
"description": "Your description",
"display": "standalone",
"name": "Your title",
"short_name": "Your short name",
"start_url": "/",
"scope": "/",
"lang": "en",
"theme_color": "white",
"icons": [
{
"src": "icon/lowres.png",
"sizes": "64x64"
}, {
"src": "icon/hd_hi",
"sizes": "128x128"
}
]
}
Service Workers
Web worker的发展已经有一段时间了,但是 service worker 对我来说还是一个比较新的概念。这两者都是为了在JS主进程之外,运行一些复杂的逻辑,从而解放主进程,提高App的运行效率。而service worker除了独立运行之外,它可以有离线缓存的能力。这些是 service worker 特性:
生命周期: service worker的生命周期比较复杂,有 registration, installation, activation等等不同的阶段。
注册:service worker必须要注册(Registration)才能使用
通信:和 web worker 一样,service worker也可以使用 postMessage() 和主进程通信;此外,service worker还有更丰富的API可以调用,如Fetch、 Cache 和 Push,因此它可以用于拦截请求,缓存文件
持久化:service worker 被 install 之后就永远存在,除非手动卸载
兼容性:现在并非所有的浏览器都兼容 service worker,目前兼容列表可以看 这里,相信之后各大浏览器厂商会提供会有更好的支持。
Push 和 Notification
作为 App, 推送和通知也是不可缺少的功能。其中,推送(Push)和通知(Notification)的概念并不一样。
- 推送:服务器将新信息推给 App,在 PWA里主要就是指推送给 service worker
- 通知:Service Worker 将更新的信息弹出,给用户以提示
PWA with Ruby on Rails
纸上得来终觉浅;PWA这件事还是要自己尝试一下才好。
我比较常用的 web 框架是 Ruby on Rails,这里我打算基于 Rails 做一点 PWA 的示例。在示例中,我打算一切从简(主要原因是 Rails 有一套自己的 assets 打包机制,这套机制在不同版本 Rails 变化比较大,所以我打算使用最直白的方式)。如果你正在使用 webpacker的话,可以尝试下这个gem serviceworker-rails。
从 Manifest 开始
我们在 public 目录下写入 manisfest.json 这个文件,icons部分我就简单写一下:
{
"name":"Hegwin.me",
"description":"Hegwin's Blog",
"icons":[
{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}
],
"theme_color":"#ffffff",
"background_color":"#ffffff",
"display":"standalone",
"start_url":"/"
}
写一个service worker
在 public/service-worker.js 中:
function onInstall(event) {
console.log('[ServiceWorker]', "Installing!", event);
}
function onActivate(event) {
console.log('[ServiceWorker]', "Activating!", event);
}
function onFetch(event) {
console.log('[ServiceWorker]', "Fetching!", event);
}
self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);
你可能疑惑这里面的 self
是什么,它是Service Worker中的一个全局变量。我们一般会用到这些全局变量:
- self: 表示 Service Worker 作用域, 也是全局变量
- caches: 表示缓存
- skipWaiting: 表示强制当前处在 waiting 状态的脚本进入 activate 状态
- clients: 表示 Service Worker 接管的页面
初始化 service worker
在 app/views/layouts/application.html.erb 中:
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<%= javascript_include_tag "application", defer: true %>
<%= csrf_meta_tags %>
在 app/assets/javascripts/application.js 中:
//= require worker-init
现在我们来写一下 app/assets/javascripts/worker-init.js 这个文件:
if (navigator.serviceWorker) {
navigator.serviceWorker.register("/service-worker.js", { scope: "/" })
.then(() => navigator.serviceWorker.ready)
.then((registration) => {
if ("SyncManager" in window) {
registration.sync.register("sync-forms");
} else {
window.alert("This browser does not support background sync.")
}
}).then(() => console.log("[WorkerInit]", "Service worker registered!"));
}
看看效果吧
好像还不错,logs都成功打印出来了。但是,让我们用Lighthouse再检查一下。
虽然检查认为已经支持了PWA,但我们看的出还是有些瑕疵的:
主要问题是:
Does not set a theme color for the address bar.Failures: No
<meta name="theme-color">
tag found.Manifest doesn't have a maskable icon
优化
在 app/views/layouts/application.html.erb 中加入:
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#ffffff">
在 manifest.json 的icons中加入:
{
"icons": [
{"src":"/apple-touch-icon.png","sizes": "180x180","type":"image/png","purpose": "any maskable"},
再来看看。
这下好多了!
如果你一步步跟下来,你会发现之前说的“service worker 被 install 之后就永远存在,除非手动卸载”并非谎言,你可以一直在DevTool的Console里看到打印的logs。怎么消除它呢,方法是这样:
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for(let registration of registrations) {
registration.unregister();
}
});
Let's call it a day!