资讯专栏INFORMATION COLUMN

构建离线web应用(二)

curlyCheng / 990人阅读

摘要:提及缓存时,不仅仅是指存储,还包括浏览器内用来保存数据以供离线使用的策略。当请求成功返回时,利用返回的数据更新页面并缓存返回的数据。这种方案主要应用用户频繁手动更新内容的场景,比如用户的收件箱或者文章内容。我们打算应用的策略。

上一篇文章中,我们成功尝试使用 service workers。我们也可以在应用中缓存一些资源。这篇文章我们准备了解这些:service workers 以及缓存是如何一起配合给用户一个完美的离线体验。

在前一个章节当我们学习如何 debugger 的时候,我们了解到浏览器的缓存存储。提及缓存时,不仅仅是指存储,还包括浏览器内用来保存数据以供离线使用的策略。

在这篇文章中,我们将要:

了解社区中常见的缓存策略

尝试可用的缓存 api

做一个用来展示 Github trending project 的 demo

在 demo 中演示离线状态下利用缓存所带来的体验

缓存策略

软件工程中的每一个理论都是对同一类问题解决方案的总结,每一个都需要时间整理并被大众接受,成为推荐的解决方案。对于 PWA 的缓存策略来说同样如此。Jake Archibald 汇总了很多常用的方案,但我们只打算介绍其中一些常用的:

Install 期间缓存

这个方案我们在上一篇文章中介绍过,缓存 app shell 展示时需要的所有资源:

</>复制代码

  1. self.addEventListener("install", function(e) {
  2. console.log("[ServiceWorker] Install");
  3. e.waitUntil(
  4. caches.open(cacheName).then(function(cache) {
  5. console.log("[ServiceWorker] Caching app shell");
  6. return cache.addAll(filesToCache);
  7. })
  8. );
  9. });

缓存的资源包括 HTML 模板,CSS 文件,JavaScript,fonts,少量的图片。

缓存请求返回的数据

这个方案是指如果之前的网络请求数据被缓存了,那么就用缓存的数据更新页面。如果缓存不可用,那直接去网络请求数据。当请求成功返回时,利用返回的数据更新页面并缓存返回的数据。

</>复制代码

  1. self.addEventListener("fetch", function(event) {
  2. event.respondWith(
  3. caches.open(cacheName).then(function(cache) {
  4. return cache.match(event.request).then(function (response) {
  5. return response || fetch(event.request).then(function(response) {
  6. cache.put(event.request, response.clone());
  7. return response;
  8. });
  9. });
  10. })
  11. );
  12. });

这种方案主要应用用户频繁手动更新内容的场景,比如用户的收件箱或者文章内容。

先展示缓存,再根据请求的数据更新页面

这种方案将同时请求缓存以及服务端的数据。如果某一项在缓存中有对应的数据,好,直接在页面中展示。当网络请求的数据返回时,利用返回的数据更新页面:

</>复制代码

  1. let networkReturned = false;
  2. if ("caches" in window) {
  3. caches.match(app.apiURL).then(function(response) {
  4. if (response) {
  5. response.json().then(function(trends) {
  6. console.log("From cache...")
  7. if(!networkReturned) {
  8. app.updateTrends(trends);
  9. }
  10. });
  11. }
  12. });
  13. }
  14. fetch(app.apiURL)
  15. .then(response => response.json())
  16. .then(function(trends) {
  17. console.log("From server...")
  18. networkReturned = true;
  19. app.updateTrends(trends.items)
  20. }).catch(function(err) {
  21. // Error
  22. });

在大多数情况下,网络请求返回的数据会将从缓存中取出的数据覆盖。但在网页中,什么情况都有可能发生,有时候网络请求数据比从缓存中取数据要快。因此,我们需要设置一个 flag 来判断网络请求有没有返回,这就是上例中的 networkReturned。

缓存部分技术选型

目前有两种可持续性数据存储方案 -- Cache Storage 以及 Index DB(IDB)。

Cache Storage:在过去的一段时间里,我们依赖 AppCache 来进行缓存处理,但我们需要一个可操作性更强的 API。幸运的是,浏览器提供了 Cache 这样的一个 API,给 Service Worker 的缓存操作带来了更多的可能。并且,这个 API 同时支持 service workers 以及 web 页面。在前一篇文章中,我们已经使用过了这个 API。

Index DB:Index DB 是一个异步数据存储方案。对于这个 API 是又爱又恨,还好,像localForage这样的类库使用类似localStorage的操作方式简化了API。

Service Worker 对于这两种存储方案都提供支持。那么问题来了,什么场景下选择哪一种技术方案呢? Addy Osmani 的博客已经总结好了。

</>复制代码

  1. 对于利用 URL 可直接查看的资源,使用支持 Service Worker 的 Cache Storage。其它类型的资源,使用利用 Promise 包裹之后的 IndexedDB。
SW Precache

上文已经介绍了缓存策略以及数据缓存数据。在实战之前,还想给大家介绍一下谷歌的 SW Precache。

这个工具还有一个额外的功能:将我们之前讨论的缓存文件设置利用正则简化成一个配置对象。所有你需要做的就是在一个数组中定义缓存的项目。

让我们来尝试使用一下 precache,让其自动生成 service-worker.js。首先,我们需要在项目的根目录下新增一个 package.json 文件:

</>复制代码

  1. npm init -y

安装 sw-precache:

</>复制代码

  1. npm install --save-dev sw-precache

创建一个配置文件:

</>复制代码

  1. // ./tools/precache.js
  2. const name = "scotchPWA-v1"
  3. module.exports = {
  4. staticFileGlobs: [
  5. "./index.html",
  6. "./images/*.{png,svg,gif,jpg}",
  7. "./fonts/**/*.{woff,woff2}",
  8. "./js/*.js",
  9. "./css/*.css",
  10. "https://fonts.googleapis.com/icon?family=Material+Icons"
  11. ],
  12. stripPrefix: "."
  13. };

staticFileGlobs 里面利用正则匹配我们想要缓存的文件。只需要利用正则,比之前枚举所有的文件简单很多。

package.json 中新增一个 script 用来生成 service worker 文件:

</>复制代码

  1. "scripts": {
  2. "sw": "sw-precache --config=tools/precache.js --verbose"
  3. },

运行下面的命令即可生成 service worker 文件:

</>复制代码

  1. npm run sw

查看生成的文件,是不是很熟悉?

完成 demo

在做 web 应用离线功能之前,让我们先来完成应用的基本功能。

回到 app.js 文件,我们要在页面加载完成时去获取当前 Github 流行的项目(项目以 star 数的多少来排序):

</>复制代码

  1. (function() {
  2. const app = {
  3. apiURL: `https://api.github.com/search/repositories?q=created:%22${dates.startDate()}+..+${dates.endDate()}%22%20language:javascript&sort=stars&order=desc`
  4. }
  5. app.getTrends = function() {
  6. fetch(app.apiURL)
  7. .then(response => response.json())
  8. .then(function(trends) {
  9. console.log("From server...")
  10. app.updateTrends(trends.items)
  11. }).catch(function(err) {
  12. // Error
  13. });
  14. }
  15. document.addEventListener("DOMContentLoaded", function() {
  16. app.getTrends()
  17. })
  18. if ("serviceWorker" in navigator) {
  19. navigator.serviceWorker
  20. .register("/service-worker.js")
  21. .then(function() {
  22. console.log("Service Worker Registered");
  23. });
  24. }
  25. })()

注意 API URL 字符串中的日期。我们是这样构造的:

</>复制代码

  1. Date.prototype.yyyymmdd = function() {
  2. // getMonth is zero based,
  3. // so we increment by 1
  4. let mm = this.getMonth() + 1;
  5. let dd = this.getDate();
  6. return [this.getFullYear(),
  7. (mm>9 ? "" : "0") + mm,
  8. (dd>9 ? "" : "0") + dd
  9. ].join("-");
  10. };
  11. const dates = {
  12. startDate: function() {
  13. const startDate = new Date();
  14. startDate.setDate(startDate.getDate() - 7);
  15. return startDate.yyyymmdd();
  16. },
  17. endDate: function() {
  18. const endDate = new Date();
  19. return endDate.yyyymmdd();
  20. }
  21. }

yyyymmdd 帮我们将日期构造成 Github API 所规定的格式(yyyy-mm-dd)。

getTrends 获取数据之后,调用了 updateTrends 方法,传入获取到的数据。让我们看看这个方法做了些什么:

</>复制代码

  1. app.updateTrends = function(trends) {
  2. const trendsRow = document.querySelector(".trends");
  3. for(let i = 0; i < trends.length; i++) {
  4. const trend = trends[i];
  5. trendsRow.appendChild(app.createCard(trend));
  6. }
  7. }

遍历请求返回的数据,利用 createCard 来创建 DOM 模板,然后,将这段 DOM 插入 .trends 元素:

</>复制代码

createCard 利用下面的代码来创建模板:

</>复制代码

  1. const app = {
  2. apiURL: `...`,
  3. cardTemplate: document.querySelector(".card-template")
  4. }
  5. app.createCard = function(trend) {
  6. const card = app.cardTemplate.cloneNode(true);
  7. card.classList.remove("card-template")
  8. card.querySelector(".card-title").textContent = trend.full_name
  9. card.querySelector(".card-lang").textContent = trend.language
  10. card.querySelector(".card-stars").textContent = trend.stargazers_count
  11. card.querySelector(".card-forks").textContent = trend.forks
  12. card.querySelector(".card-link").setAttribute("href", trend.html_url)
  13. card.querySelector(".card-link").setAttribute("target", "_blank")
  14. return card;
  15. }

下面就是所创建的 DOM 结构:

</>复制代码

  1. Card Title
  2. info JavaScript
  3. star 299
  4. assessment 100
  5. A set of best practices for JavaScript projects

  6. Visit Repo

运行时缓存的内容

在应用程序运行时,需要缓存从服务端获取的动态内容。不再是 app shell 了,而是用户真正浏览的内容。

我们需要提前配置告诉 service worker ,在运行时需要缓存的文件:

</>复制代码

  1. // ./tools/precache.js
  2. const name = "scotchPWA-v1"
  3. module.exports = {
  4. staticFileGlobs: [
  5. // ...
  6. ],
  7. stripPrefix: ".",
  8. // Run time cache
  9. runtimeCaching: [{
  10. urlPattern: /https://api.github.com/search/repositories/,
  11. handler: "networkFirst",
  12. options: {
  13. cache: {
  14. name: name
  15. }
  16. }
  17. }]
  18. };

我们定义了一个 url 正则匹配符,匹配成功时,读取缓存。这个正则匹配所有的 Github 搜索 API。我们打算应用“Cache, Then network.”的策略。

这样,我们先展示缓存的内容,当有网络连接时候,更新内容:

</>复制代码

  1. app.getTrends = function() {
  2. const networkReturned = false;
  3. if ("caches" in window) {
  4. caches.match(app.apiURL).then(function(response) {
  5. if (response) {
  6. response.json().then(function(trends) {
  7. console.log("From cache...")
  8. if(!networkReturned) {
  9. app.updateTrends(trends);
  10. }
  11. });
  12. }
  13. });
  14. }
  15. fetch(app.apiURL)
  16. .then(response => response.json())
  17. .then(function(trends) {
  18. console.log("From server...")
  19. app.updateTrends(trends.items)
  20. networkReturned = true;
  21. }).catch(function(err) {
  22. // Error
  23. });
  24. }

precache.js 中更新缓存的版本,重新生成 service worker:

</>复制代码

  1. const name = "scotchPWA-v2"

</>复制代码

  1. npm run sw

当你运行应用的时候,尝试刷新,打开控制台,勾选 offline 选项。之后,刷新,以及见证奇迹的时刻:

刷新

用户可能需要在网络情况更佳的时候刷新页面,我们需要给予用户这样的权利。我们可以给刷新按钮添加一个事件,当时间触发时,调用 getTrends 方法:

</>复制代码

  1. document.addEventListener("DOMContentLoaded", function() {
  2. app.getTrends()
  3. // Event listener for refresh button
  4. const refreshButton = document.querySelector(".refresh");
  5. refreshButton.addEventListener("click", app.getTrends)
  6. })
下一步?

感觉不是很满足?现在你已经知道了如何创建离线应用,在接下来的文章中,我们将继续讨论这项技术的有趣之处,包括推送通知,主屏幕图标创建等等···

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93937.html

相关文章

  • Web离线技术(一)—— 技术选型

    摘要:上面提到在安卓完全不需要像这样大费周章的绕弯路,所以安卓可能就不需要这个自定义的,这样又会导致面临着与安卓差异化严重问题。前言 最早接触离线包的概念要追溯到16年初,项目迎来大改版,其中重点项目之一就是离线包方案的制定与实施。离线包顾名思义就是将H5/CSS/JS和资源文件打包提前下发到App中,这样App在加载网页的时候实际上加载的是本地的文件,减少网络请求来提高网页的渲染速度,并实现动态...

    null1145 评论0 收藏0
  • 构建离线WEB应用

    摘要:使用离线应用构建应用服务端服务器配置创建文件客户端构建,并在标签上添加属性,属性值是服务器上配置的缓存资源列表的文件名配置相关事件,创建离线文件内容将状态代码转化成状态离线应用创建即使没有互联网连接也可以使用的应用程序。 HTML5新增了localstroage和application cache做离线缓存,两种缓存各有应用的场景,今天我们说说application cache这种方案...

    shleyZ 评论0 收藏0
  • 构建离线WEB应用

    摘要:使用离线应用构建应用服务端服务器配置创建文件客户端构建,并在标签上添加属性,属性值是服务器上配置的缓存资源列表的文件名配置相关事件,创建离线文件内容将状态代码转化成状态离线应用创建即使没有互联网连接也可以使用的应用程序。 HTML5新增了localstroage和application cache做离线缓存,两种缓存各有应用的场景,今天我们说说application cache这种方案...

    lk20150415 评论0 收藏0
  • 构建离线web应用(一)

    摘要:我喜欢移动,而且也是那些坚持使用技术构建移动应用程序的人之一。我们准备做这样的一个渐进式应用是典型的旨在提高用户离线体验的应用。当我们开始构建应用时,你就能理解上面的场景了。的作用范围是针对相对路径的。最佳的做法是在应用的入口。 我喜欢移动app,而且也是那些坚持使用Web技术构建移动应用程序的人之一。 经过技术的不断迭代(可能还有一些其它的东西),移动体验设计愈来愈平易近人,给予用户...

    Sanchi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<