PWA踩坑记-从零到一让你的博客也能离线访问

前言

这篇文章所采用的chrome浏览器版本为72,如有疑虑,请留言讨论,谢谢。

您将在这篇文章中学习到:

  • 什么是PWA
  • PWA的工作原理
  • 如何给自己的hexo静态博客实现PWA
  • Service worker生命周期
  • Service worker自动更新

什么是PWA

PWA,即Progressive-Web-App,渐进式网络应用,概念有点类似于微信小程序,和MIUI的快应用,但是比他们更简单,没有平台依赖性,你只需在浏览器中访问支持PWA的网页,就能收到安装提示。

PWA可以

  • 让你离线浏览内容,摆脱对于网络的束缚
  • 提示安装到桌面,提升用户粘性/依赖度
  • 即使没有打开网页,也能主动推送通知
  • 即刻安装,免除对于存储空间的考虑

最终效果一览

手机

安卓手机Chrome浏览器最终效果

Chrome弹窗提示

Chrome正在将网站添加到桌面

MIUI浏览器提示添加到桌面

在手机上打开的splash第一屏

PC

Toast 提示离线

windows Chrome 提示安装

在chrome菜单项选择打开

可以选择在chrome中打开或卸载

打开之后的样子

PWA工作原理

W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓存

浏览器中的 javaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。随着 Web 业务不断复杂,我们逐渐在 js 中加了很多耗资源、耗时间的复杂运算过程,这些过程导致的性能问题在 WebApp 的复杂化过程中更加凸显出来。

Service Worker 有以下功能和特性:

  • 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
  • 一旦被 install,就永远存在,除非被手动 unregister
  • 用到的时候可以直接唤醒,不用的时候自动睡眠
  • 可编程拦截代理请求和返回缓存文件,缓存的文件可以被网页进程fetch
  • 离线内容开发者可控
  • 能向客户端推送消息
  • 不能直接操作 DOM
  • 必须在 HTTPS 环境下才能工作
  • 异步实现,内部大都是通过Promise实现

所以我们基本上知道了 Service Worker 的伟大使命,就是让缓存做到优雅和极致,让 Web App 相对于 Native App 的缺点更加弱化,也为开发者提供了对性能和体验的无限遐想。[1]

service worker 生命周期

service worker 所有支持的事件

实现过程

Chrome 将在您的应用符合以下条件时自动显示横幅(添加到桌面的提示):

  • 拥有一个网络应用清单文件(manifest.json),该文件至少具有:
    • 一个 short_name(用于主屏幕)
    • 一个 name(用于横幅中)
    • 一个 192x192 png 图标(图标声明必须包含一个 mime 类型的 image/png
    • 一个加载的 start_url
  • 拥有一个在您的网站上注册的服务工作线程(service worker)
  • 通过HTTPS提供(这是使用服务工作线程的一项要求)。
  • 被访问至少两次(第一次访问执行安装和激活,第二次访问生效,提示安装到桌面),这两次访问至少间隔五分钟。

创建manifest.json文件

在您博客的根目录下面的source文件夹创建一个manifest.json

  • 在用户主屏幕上用作文本的 short_name
  • 在网络应用安装横幅中使用的 name
  • 利用适当命名的 background_color 属性指定背景颜色。 Chrome 在网络应用启动后会使用此颜色和name和512x512的icon展示第一屏。
  • 使用 theme_color 在您hexo博客的主题的layout.ejs等文件,属性指定主题颜色。
<meta name="theme-color" content="#EEEEEE">
{
"name": "告你什么文集",
"short_name": "告你什么",
"icons": [{
"src": "/128x128-logo.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "/144x144-logo.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "/152x152-logo.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "/192x192-logo.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/256x256-logo.png",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "/512x512-logo.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "/",
"display": "standalone",
"background_color": "#0f9d58",
"theme_color": "#0f9d58",
"scope": "/"
}

display

source文件夹下面的文件会被hexo解析,如果您有插件会处理这些文件(如:hexo-translate-title,建议您在博客项目的_config.yml文件skip_render项添加一个子项:

skip_render: ['manifest.json']

skip_render: 
- manifest.json

这两种写法都是正确的,可以确保manifest.json不被hexo和其他插件解析,更改内容;而且放在source目录下面的文件,在执行hexo ghexo s后,最终都会复制到网站根目录。🙃

将manifest的相关信息告知浏览器

在您创建清单文件之后,将 link 标记添加到layout.ejs页面上,如下所示:

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

在您主题的layout.ejs中添加一些meta标签

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="告你什么">
<meta name="msapplication-TileImage" content="/144x144-logo.png">
<meta name="msapplication-TileColor" content="#0f9d58">
<link rel="apple-touch-icon" href="/152x152-logo.png">

其中的apple-mobile-web-app-status-bar-style值,可以设为:default, black or black-translucent

If set to black-translucent, the status bar is black and translucent. If set to default or black, the web content is displayed below the status bar. If set to black-translucent, the web content is displayed on the entire screen

更多详情请参考:https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html

apple-mobile-web-app-status-bar-style

创建service worker

在您博客的根目录下面创建一个文件夹,暂时叫他static_files,在里面创建一个js文件:sw.js

目的是使用hexo事件,动态生成带有时间戳的sw.js到最终的输出的public目录去

PS:sw.js只能放在网站根目录下,它的作用域就是您sw.js文件放置的目录。

// 使用{uniqueIdentifier}模板,稍后我们将使用hexo的事件机制,替换成ISO时间,作为每次构建的唯一标识符
var cacheName = 'tellyouwhat-cache-{uniqueIdentifier}';
// 在这个数组里面写入您主页加载需要的资源文件
var filesToCache = [
'/',
'/categories/',
'/tags/',
'/archives/',
'/about/',
'/js/matery.js',
'/js/search.js',
'/css/matery.css',
'/css/my-gitalk.css',
'/page/2/',
'/search.xml',
'/favicon.png',
'/144x144-logo.png',
'/manifest.json',
'/css/prism-atom-dark.css',
];

self.addEventListener('install', e => {
e.waitUntil(
caches.open(cacheName).then(cache => {
return cache.addAll(filesToCache)
.then(() => self.skipWaiting());
})
);
});

self.addEventListener('activate', function (e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(keyList.map(function (key) {
if (key !== cacheName) {
// 清理旧版本
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
// 更新客户端
return self.clients.claim();
});

self.addEventListener('fetch', event => {
event.respondWith(
caches.open(cacheName)
.then(cache => cache.match(event.request, {ignoreSearch: true}))
.then(response => {
// 使用缓存而不是进行网络请求,实现app秒开
return response || fetch(event.request);
})
);
});
  1. install 事件会绑定在 Service Worker 文件中,在 Service Worker 安装成功后,install 事件被触发。install事件一般是被用来填充浏览器的离线缓存能力。为了达成这个目的,使用了 cache API ,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成key。它会一直持久存在,直到你告诉它不再存储,你拥有全部的控制权。
  2. 当 service worker 安装完成后,会接收到一个激活事件(activate event)。 activate主要用途是清理先前版本的service worker 脚本中使用的资源。
  3. fetch 事件在您刷新网页的时候,会在您的缓存里面找,如果没有缓存才去做http请求。

更新你的service worker

如果您的 service worker 已经被安装,但是刷新页面时有一个新版本的可用,新版的 service worker 会在后台安装,但是还没激活。当不再有任何已加载的页面在使用旧版的 service worker 的时候,新版本才会激活(也就是说,关闭浏览器窗口之后,再重新打开)。一旦再也没有更多的这样已加载的页面,新的 service worker 就会被激活。

这也就是为什么我们要动态生成cacheName,让他以时间为后缀。

在service worker 里的 install 事件监听器里面,删除旧的缓存。

缓存策略

启用service worker

在您博客的js文件中,加入以下代码:

// init PWA
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(function () {
console.log("Service Worker Registered");
});
}

这样就能成功注册了。

主动提示用户添加到桌面

在chrome68之前,会自动提示,但是在68之后,必须开发人员监听beforeinstallprompt事件,来提示用户安装到桌面:

请使用deferredPrompt延迟的提示,保存提示事件。PS:如果在beforeinstallprompt里面直接调用e.prompt()函数,会报错(prompt is not a function)没有此函数。

let deferredPrompt;

window.addEventListener('beforeinstallprompt', function (e) {
console.log('before install prompt')
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
showAddToHomeScreen();
});

下面代码使用Materialize框架的toast功能,展示一个弹窗提醒用户安装:

function showAddToHomeScreen() {
let toastHTML = '<span>使<i>告你什么</i>可以离线访问?</span><button class="btn-flat toast-action" onclick="addToHomeScreen()">Yes</button>';
M.toast({html: toastHTML});
}

或自定义一个按钮来让用户主动触发安装(您需要在页面上写一个和自己代码相匹配的,如class="ad2hs-prompt"的标签)[2]

function showAddToHomeScreen() {
var a2hsBtn = document.querySelector(".ad2hs-prompt");
a2hsBtn.style.display = "block";
a2hsBtn.addEventListener("click", addToHomeScreen);
}

真正的Chrome级提示用户安装[2:1]

function addToHomeScreen() {
deferredPrompt.prompt(); // Wait for the user to respond to the prompt
deferredPrompt.userChoice
.then(function (choiceResult) {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
// 释放不再有用的deferredPrompt对象
deferredPrompt = null;
});
}

使用hexo的generateAfter事件生成sw.js

在您博客的根目录下创建scripts文件夹,在里面创建events.js

PS:这个是hexo的固定写法,hexo会在生成网站之前执行项目根目录下scripts文件夹下的所有js脚本。[3]

var fs = require('fs');

// 在generate之后把sw.js处理输出到public文件夹
hexo.on('generateAfter', function () {
if (!fs.existsSync('./public')) {
// 如果文件夹不存在就创建一个
fs.mkdirSync('./public')
}

// 把sw.js文件中的{uniqueIdentifier}替换成构建时的时间标识符
fs.writeFile('./public/sw.js',
fs.readFileSync('./static_files/sw.js').toString().replace('{uniqueIdentifier}', new Date().toISOString()),
function (err) {
if (err) {
console.error(err)
} else {
console.log('service worker created')
}
})
})

这时候再执行hexo g,就能看到public文件夹下面生成了sw.js,打开即可看见第一行类似于这样:

var cacheName = 'tellyouwhat-cache-2019-03-13T11:32:42.507Z';

这样浏览器在获取到这个sw.js的时候,就能知道网站被更新了,而且缓存也会被更新。

测试

lighthouse

lighthouse

运行 Lighthouse 的方式有两种:作为 Chrome 扩展程序运行,或作为命令行工具运行。 Chrome 扩展程序提供了一个对用户更友好的界面,方便读取报告。 命令行工具允许您将 Lighthouse 集成到持续集成系统。[4]

Chrome 扩展程序

安装 Lighthouse Chrome 扩展程序

转到您要进行审查的页面。

点击位于 Chrome 工具栏上的 Lighthouse 图标。

接下来就会进行自动化的web测试

测试成功

如果您是第一次做PWA应用,十有六七会失败在这一步,请您按照lighthouse的指示优化您的网页,这样才能正常使用,向用户主动提示安装到桌面。

使用F12审查

在浏览器中按下F12,进入Audits选项卡,点击开始审查

Audits标签

命令行工具

安装 Node,需要版本 5 或更高版本。[4:1]

安装 Lighthouse 作为一个全局节点模块。

$ npm install -g lighthouse

针对一个页面运行 Lighthouse 审查。

$ lighthouse https://tellyouwhat.cn/

审查结束后自动打开审查报告

$ lighthouse https://tellyouwhat.cn/ --view

其他工具

  1. PWA检查列表,包含对于PWA应用详细且近乎严苛的要求:

    https://developers.google.com/web/progressive-web-apps/checklist

  2. 网速测试工具,使您的网页在所有设备上都能快速加载:

    https://developers.google.com/speed/pagespeed/insights/

  3. 测试网页性能,Test a website’s performance:

    https://www.webpagetest.org/

结语

  1. PWA目前在苹果设备上支持非常差,只能由用户主动点击分享按钮,添加到桌面,而且在这个图标里面点击任意a标签,都会打开safari浏览器,用户体验极差。(iOS12.2以前版本,iOS12.2修复了都会打开safari浏览器的这个用户体验问题)
  2. PWA目前在安卓手机上安装后,可以把apk提取出来,发送给朋友,如果朋友没有安装Chrome浏览器,则会被提示需要安装Chrome浏览器才能继续。

我们将在下一篇文章中介绍如何摆脱需要Chrome浏览器的限制。

其他文章

  1. TWA踩坑记-从零到一让你的博客变成app

参考资料


  1. Service Worker 简介 https://lavas.baidu.com/pwa/offline-and-cache-loading/service-worker/service-worker-introduction ↩︎

  2. How to Use the ‘beforeinstallprompt’ 🔔 Event to Create a Custom PWA Add to Homescreen Experience 👍 https://love2dev.com/blog/beforeinstallprompt/ ↩︎ ↩︎

  3. https://hexo.io/zh-cn/docs/themes#scripts ↩︎

  4. 使用 Lighthouse 审查网络应用 https://developers.google.com/web/tools/lighthouse/ ↩︎ ↩︎


   转载规则


《PWA踩坑记-从零到一让你的博客也能离线访问》Harbor Zeng 采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
 上一篇
如何更新CircleCI的缓存 如何更新CircleCI的缓存
前言 众所周知,CircleCI的缓存是不可变的(immutable),如果不跟后缀保存缓存的话,会报错如下: Skipping cache generation, cache already exists for key 如何解决这样的错误,也就是如何更新现有的缓存,是一个急需解决的问题。 现状 在初始构建运行之后,有了缓存,未来的构建将运行得更快。 common case steps:
下一篇 
如何搭配一个无与伦比的主题 如何搭配一个无与伦比的主题
前言 在这篇文章中您将学到: 如何创建一篇文章 什么是front-matter 如何下载主题 如何启用主题 创建一篇文章 $ hexo new "关于一个人住,享受孤独的一些感受"INFO Created: 添加博文内容 ---title: 史上最全hexo静tags: - hexoimg: https://tellyouwhat-static-125199583
  目录