草庐IT

缓存&PWA实践

袋鼠云数栈前端 2023-03-28 原文

缓存&PWA 实践

一、背景

从上一篇《前端动画实现与原理分析》,我们从 Performance 进行动画的性能分析,并根据 Performance 分析来优化动画。但,前端不仅仅是实现流畅的动画。ToB 项目会经常与数据的保存、渲染打交道。例如开发中,为了提高用户体验,遇到了一些场景,其实就是在利用某些手段,来进行性能优化。

  • Select 下拉:做前端分页展示 → 避免一次性渲染数据造成浏览器的假死状态;
  • indexedDB:存储数据 → 用户下一次进入时,保存上一次编辑的状态 ……

那不免引发思考,我们从缓存与数据存储来思考,该如何优化?

二、 HTTP 缓存

是什么?

Http 缓存其实就是浏览器保存通过 HTTP 获取的所有资源,
是浏览器将网络资源存储在本地的一种行为。

请求的资源在哪里?

  1. 6.8kB + 200 状态码: 没有命中缓存,实际的请求,从服务器上获取资源;
  2. memory cache: 资源缓存在内存中,不会请求服务器,一般已经加载过该资源且缓存在内存中,当关闭页面时,此资源就被内存释放掉了;
  3. disk cache: 资源缓存在磁盘中,不会请求服务器,但是该资源也不会随着关闭页面就释放掉;
  4. 304 状态码:请求服务器,发现资源没有被更改,返回 304 后,资源从本地取出;
  5. service worker: 应用级别的存储手段;

HTTP 缓存分类

1. 强缓存

  1. 浏览器加载资源时,会先根据本地缓存资源的 header 中的信息判断是否命中强缓存。如果命中,则不会像服务器发送请求,而是直接从缓存中读取资源。
  2. 强缓存可以通过设置 HTTP Header 来实现:
    http1.0 → Expires: 响应头包含日期/时间, 即在此时候之后,响应过期。
    http1.1 → Cache-Control:max-age=: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间

? Cache-control

  • cache-control: max-age=3600 :表示相对时间
  • cache-control:no-cache → 可以存储在本地缓存的,只是在原始服务器进行新鲜度在验证之前,缓存不能将其提供给客户端使用
  • cache-control: no-store → 禁止缓存对响应进行复制,也就是真正的不缓存数据在本地;
  • catch-control:public → 可以被所有用户缓存(多用户共享),包括终端、CDN 等
  • cache-control: private → 私有缓存

2. 协商缓存

  1. 当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求返回的 http 状态 304,并且会显示 Not Modified 的字符串;
  2. 协商缓存通过【last-Modified,if-Modified-Since】与【ETag, if-None-Match】来管理的。

  • 「Last-Modified、If-Modified-Since」

last-Modified : 表示本地文件最后修改的日期,浏览器会在请求头中带上 if-Modified-since(也是上次返回的 Last-Modified 的值),服务器会将这个值与资源修改的时间进行匹配,如果时间不一致,服务器会返回新的资源,并且将 Last-modified 值更新,并作为响应头返回给浏览器。如果时间一致,表示资源没有更新,服务器会返回 304 状态,浏览器拿到响应状态码后从本地缓存中读取资源。

  • ETag、If-None-Match」

http 1.1 中, 服务器通过 Etag 来设置响应头缓存标示。Etag 是由服务器来生成的。

第一次请求时,服务器会将资源和 ETag 一并返回浏览器,浏览器将两者缓存到本地缓存中。

第二次请求时,浏览器会将 ETag 的值放到 If-None-Match 请求头去访问服务器,服务器接收请求后,会将服务器中的文件标识与浏览器发来的标识进行比对,如果不同, 服务器会返回更新的资源和新的 Etag,如果相同,服务器会返回 304 状态码,浏览器读取缓存。

? 思考为什么有了 Last-Modified 这一对儿,还需要 Etag 来标识是否过期进行命中协商缓存

  1. 文件的周期性更改,但是文件的内容没有改变,仅仅改变了修改时间,这个时候,并不希望客户端认为该文件被修改了,而重新获取。
  2. 如果文件文件频繁修改,比如 1s 改了 N 次,If-Modified-Since 无法判断修改的,会导致文件已经修改但是获取的资源还是旧的,会存在问题。
  3. 某些服务器不能精确得到文件的最后修改时间,导致资源获取的问题。

⚠️  如果 Etag 与 Last-Modified 同时存在,服务器会先检查 ETag,然后在检查 Last-Modified, 最终确定是返回 304 或 200。

HTTP 缓存实践

测试环境: 利用 Koa,搭建一个 node 服务,用来测试如何命中强缓存还是协商缓存

当 index.js 没有配置任何关于缓存的配置时, 无论怎么刷新 chrome,都没有缓存机制的;

  • 注意⚠️:在开始实验之前要把 network 面板的 Disable cache 勾选去掉,这个选项表示禁用浏览器缓存,浏览器请求会带上 Cache-Control: no-cache 和 Pragma: no-cache 头部信息。

1. 命中强缓存

app.use(async (ctx) => {
    // ctx.body = 'hello Koa'
    const url = ctx.request.url;
    if(url === '/'){
        // 访问跟路径返回 index.html
        ctx.set('Content-type', 'text/html');
        ctx.body = await parseStatic('./index.html')
    }else {
        ctx.set('Content-Type', parseMime(url))
        ctx.set('Expires', new Date(Date.now() + 10000).toUTCString()) // 实验1
        ctx.set('Cache-Control','max-age=20') // 实验2
        ctx.body = await parseStatic(path.relative('/', url))
    }
})

app.listen(3000, () => {
    console.log('starting at port 3000')
})

2. 命中协商缓存

         /**
         * 命中协商缓存
         * 设置 Last-Modified, If-Modified-Since
         */
         ctx.set('cache-control', 'no-cache'); // 关闭强缓存
         const isModifiedSince = ctx.request.header['if-modified-since'];
         const fileStat = await getFileStat(filePath);
         if(isModifiedSince === fileStat.mtime.toGMTString()){
             ctx.status = 304
         }else {
             ctx.set('Last-Modified', fileStat.mtime.toGMTString())
         }  
         ctx.body = await parseStatic(path.relative('/', url))

        /**
         * 命中协商缓存
         * 设置 Etag, If-None-Match
         */
         ctx.set('cache-control', 'no-cache');
         const ifNoneMatch = ctx.request.headers['if-none-match'];
         const fileBuffer = await parseStatic(filePath);
         const hash = crypto.createHash('md5');
         hash.update(fileBuffer);
         const etag = `"${hash.digest('hex')}"`
         if (ifNoneMatch === etag) {
            ctx.status = 304
          } else {
            ctx.set('Etag', etag)
            ctx.body = fileBuffer
          }
    }

三、 浏览器缓存

1.Cookies

  • MDN 定义: 是服务器发送到用户浏览器并报讯在本地的一小块数据,他会在浏览器下次想统一服务器再次发送请求时被携带并发送到服务器上。
  • 应用场景:
    • 会话状态管理【用户登陆状态,购物车,游戏分数或其他需要记录的信息】
    • 个性化设置(如用户自定义设置、主题等)
    • 浏览器行为跟踪(如跟踪分析用户行为等)
  • cookie 的读取与写入:
class Cookie {
	getCookie: (name) => {
		const reg = new RegExp('(^| )'+name+'=([^;]*)')
		let match = document.cookie.match(reg); //  [全量,空格,value,‘;’]
		if(match) {return decodeURI(match[2])}
	}
	setCookie:(key,value,days,domain) => {
		// username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/
	  let d = new Date();
    d.setTime(d.getTime()+(days*24*60*60*1000));
    let expires = "; expires="+d.toGMTString();
		let domain = domain ? '; domain='+domain : '';
		document.cookie = name + '=' + value + expires + domain + '; path=/'
		
	}
	deleteCookie: (name: string, domain?: string, path?: string)=> {
		// 删除cookie,只需要将时间设置为过期时间,而无需删除cookie的value
        const d = new Date(0);
        domain = domain ? `; domain=${domain}` : '';
        path = path || '/';
        document.cookie =
            name + '=; expires=' + d.toUTCString() + domain + '; path=' + path;
    },
}
  • 存在的问题: 由于通过 Cookie 存储的数据,每次请求都会携带在请求头。对与一些数据是无需交给提交后端的,这个不免会带来额外的开销。

2.WebStorage API

浏览器能以一种比使用 Cookie 更直观的方式存储键值对

localStorage

为每一个给定的源(given origin)维持一个独立的存储区域,浏览器关闭,然后重新打开后数据仍然存在。

sessionStorage

为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。

3.indexedDB 与 webSQL

webSQL

基本操作与实际数据库操作基本一致。
最终的数据去向,一般只是做临时存储和大型网站的业务运行存储缓存的作用,页面刷新后该库就不存在了。而其本身与关系数据库的概念比较相似。

indexedDB

随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。现有的浏览器数据储存方案,都不适合储存大量数据;

IndexedDB 是浏览器提供的本地数据库, 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。


四、应用程序缓存

Service Worker

在提及 Service Worker 之前,需要对 web Worker 有一些了解;

webWorker : Web Worker 是浏览器内置的线程所以可以被用来执行非阻塞事件循环的 JavaScript 代码。 js 是单线程,一次只能完成一件事,如果出现一个复杂的任务,线程就会被阻塞,严重影响用户体验, Web Worker 的作用就是允许主线程创建 worker 线程,与主线程同时进行。worker 线程只需负责复杂的计算,然后把结果返回给主线程就可以了。简单的理解就是,worker 线程执行复杂计算并且页面(主线程)ui 很流畅,不会被阻塞。


Service Worker 是浏览器和网络之间的虚拟代理。其解决了如何正确缓存往后网站资源并使其在离线时可用的问题。

Service Worker 运行在一个与页面 js 主线程独立的线程上,并且无权访问 DOM 结构。他的 API 是非阻塞的,并且可以在不同的上下文之间发送和接收消息。

他不仅仅提供离线功能,还可以做包括处理通知、在单独的线程上执行繁重的计算等事务。Service Workers 非常强大,因为他们可以控制网络请求,修改网络请求,返回缓存的自定义响应或者合成响应。

2.PWA

? PWA,全称 Progressive web apps,即渐进式 Web 应用。PWA 技术主要作用为构建跨平台的 Web 应用程序,并使其具有与原生应用程序相同的用户体验。
? PWA 的核心: 最根本、最基本的,就是 Service Worker 以及在其内部使用的 Cache API。只要通过 Service Worker 与 Cache API,实现了对网站页面的缓存、对页面请求的拦截、对页面缓存的操纵 。

为什么使用 PWA:

传统的 Web 存在的问题:

  1. 缺乏直接入口,需要记住他的域名,或者是保存在收藏夹,寻找起来不够方便;
  2. 依赖于网络。只要客户端处于断网的状态,整个 web 就处于瘫痪状态,客户端无法使用;
  3. 无法像 Native APP 推送消息。

传统 Native APP 存在的问题:

  1. 需要安装与下载。哪怕只是使用 APP 的某个功能,也是需要全盘下载的;
  2. 开发成本高,一般需要兼容安卓与 IOS 系统;
  3. 发布需要审核;
  4. 更新成本高……

PWA 的存在,就是为了解决以上问题所带来的麻烦:
优势:

  1. 桌面入口,打开便捷;
  2. 离线可用,不用过度依赖网络;
  3. 安装方便;
  4. 一次性开发,无需审核,所有平台可用;
  5. 能够进行消息推送
  • Web App Manifest Web App Manifest(Web 应用程序清单)概括地说是一个以 JSON 形式集中书写页面相关信息和配置的文件。
{
  "short_name": "User Mgmt",
  "name": "User Management",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".", // 调整网站的起始链接
  "display": "standalone", // 设定网站提示模式 : standalone 表示隐藏浏览器的UI
  "theme_color": "#000000", // 设定网站每个页面的主题颜色
  "background_color": "#ffffff" // 設定背景顏色
}
  • ServiceWorker.js 代码
/* eslint-disable no-restricted-globals */

// 确定哪些资源需要缓存
const CACHE_VERSION = 0;
const CACHE_NAME = 'cache_v' + CACHE_VERSION;
const CACHE_URL = [
  '/',
  'index.html',
  'favicon.ico',
  'serviceWorker.js',
  'static/js/bundle.js',
  'manifest.json',
  'users'
]
const preCache = () => {
  return caches
    .open(CACHE_NAME)
    .then((cache) => {
      return cache.addAll(CACHE_URL)
    })
}
const clearCache = () => {
  return caches.keys().then(keys => {
    keys.forEach(key => {
      if (key !== CACHE_NAME) {
        caches.delete(key)
      }
    })
  })
}
// 进行缓存
self.addEventListener('install', (event) => {
  event.waitUntil(
    preCache()
  )
})

// 删除旧的缓存
self.addEventListener('activated', (event) => {
  event.waitUntil(
    clearCache()
  )
})

console.log('hello, service wold');

self.addEventListener('fetch', (event) => {
  console.log('request:', event.request.url)
  event.respondWith(
    fetch(event.request).catch(() => { // 优先网络请求,如果失败,则从缓存中拿资源
      return caches.match(event.request)
    })
  )
})
  • PWA 调试

当离线的时候依然拿到缓存的资源,并且正常展示,可以看出资源被 serviceWorker 缓存。

借助开发者工具:
chrome devtools : chrome://inspect/#service-workers ,可以展示当前设备上激活和存储的 service worker

五、总结与思考

参考优秀网站:

  1. 语雀: https://www.yuque.com/dashboard
  2. PWA 例子: https://mdn.github.io/pwa-examples/js13kpwa/

有关缓存&PWA实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  3. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  4. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  5. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  6. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  9. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  10. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

随机推荐