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

前言

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

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

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

什么是PWA

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

PWA可以

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

最终效果一览

手机

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

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

Chrome弹窗提示

Chrome弹窗提示

Chrome正在将网站添加到桌面

Chrome正在将网站添加到桌面

MIUI浏览器提示添加到桌面

MIUI浏览器提示添加到桌面

在手机上打开的splash第一屏

在手机上打开的splash第一屏

PC

Toast 提示离线

Toast 提示离线

windows Chrome 提示安装

windows Chrome 提示安装

在chrome菜单项选择打开

在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 生命周期

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等文件,属性指定主题颜色。

HTML

<meta name="theme-color" content="#EEEEEE">

JSON

{
"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

display

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

YAML

skip_render: ['manifest.json']

YAML

skip_render:
- manifest.json

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

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

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

HTML

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

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

HTML

<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

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

创建service worker

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

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

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

JAVASCRIPT

// 使用{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文件中,加入以下代码:

JAVASCRIPT

// 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)没有此函数。

JAVASCRIPT

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功能,展示一个弹窗提醒用户安装:

JAVASCRIPT

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]

JAVASCRIPT

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

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

JAVASCRIPT

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]

JAVASCRIPT

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,打开即可看见第一行类似于这样:

JAVASCRIPT

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

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

测试

lighthouse

lighthouse

lighthouse

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

Chrome 扩展程序

安装 Lighthouse Chrome 扩展程序

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

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

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

测试成功

测试成功

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

使用F12审查

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

Audits标签

Audits标签

命令行工具

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

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

POWERSHELL

$ npm install -g lighthouse

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

POWERSHELL

$ lighthouse https://tellyouwhat.cn/

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

POWERSHELL

$ 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 国际许可协议 进行许可。
Comments
  • Latest
  • Oldest
  • Hottest
No comment yet.
Powered by Waline v2.15.8
 上一篇
如何更新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