浏览器基础(4)-存储的前身后世

cookie,localStorage,sessionStorage,indexDB
Cookie

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

Cookie主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie曾一度用于客户端数据的存储,因当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie渐渐被淘汰。由于服务器指定Cookie后,浏览器的每次请求都会携带Cookie数据,会带来额外的性能开销(尤其是在移动环境下)。

创建Cookie

服务器收到HTTP请求时,服务器可以在响应头里面添加一个Set-Cookie选项。浏览器收到响应后通常会保存下Cookie,之后对该服务器每一次请求中都通过Cookie请求头部将Cookie信息发送给服务器。另外,Cookie的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。

Cookie的Secure 和HttpOnly 标记

标记为 Secure 的Cookie只应通过被HTTPS协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过Cookie传输,因为Cookie有其固有的不安全性,Secure 标记也无法提供确实的安全保障。从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:)无法使用Cookie的 Secure 标记。

为避免跨域脚本 (XSS) 攻击,通过JavaScript的 Document.cookie API无法访问带有 HttpOnly 标记的Cookie,它们只应该发送给服务端。如果包含服务端 Session 信息的 Cookie 不想被客户端 JavaScript 脚本调用,那么就应该为其设置 HttpOnly 标记。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
Cookie的作用域

Domain 和 Path 标识定义了Cookie的作用域:即Cookie应该发送给哪些URL。

Domain 标识指定了哪些主机可以接受Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了Domain,则一般包含子域名。

例如,如果设置 Domain=mozilla.org,则Cookie也包含在子域名中(如developer.mozilla.org)。

Path 标识指定了主机下的哪些路径可以接受Cookie(该URL路径必须存在于请求URL中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。

例如,设置 Path=/docs,则以下地址都会匹配:

  • /docs
  • /docs/Web/
  • /docs/Web/HTTP
SameSite Cookies

SameSite Cookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。

下面是例子:

Set-Cookie: key=value; SameSite=Strict

SameSite可以有下面三种值:

  • None 浏览器会在同站请求、跨站请求下继续发送cookies,不区分大小写。
  • Strict 浏览器将只发送相同站点请求的cookie(即当前网页URL与请求目标URL完全一致)。如果请求来自与当前location的URL不同的URL,则不包括标记为Strict属性的cookie。
  • Lax 在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者frames的调用,但只有当用户从外部站点导航到URL时才会发送。如link链接
JavaScript通过Document.cookie访问Cookie

通过Document.cookie属性可创建新的Cookie,也可通过该属性访问非HttpOnly标记的Cookie。

document.cookie = "yummy_cookie=choco"; 
document.cookie = "tasty_cookie=strawberry"; 
console.log(document.cookie); 
// logs "yummy_cookie=choco; tasty_cookie=strawberry"
基于cookie的登陆验证与退出

怎么实现退出登陆,页面跳转到登陆页面,前端登陆后,后端返回字段设置cookie 就可以实现身份认证,但是这个cookies 应该是设置了httponly 字段,不允许前端js操作的,那点击退出按钮怎么应该做什么

首先先解决这样一个疑问,就是不论cookie有没有设置httponly属性,登陆或者退出时候的cookie都不应该由js来操作。

网站发送登陆请求之后,在响应头中通过Set-Cookie来设置cookie,浏览器接收到响应后,会将Set-Cookie中的cookie信息存储到浏览器:
在这里插入图片描述
这是登陆的情况,那退出呢?这时有些朋友认为,点击退出按钮,或者进行退出操作,直接调用js删除cookie不就可以了吗,一般的项目中是不会这样操作的,删除cookie也是通过后端来实现。既然后端可以通过Set-Cookie设置cookie,那么也应该可以通过Set-Cookie删除cookie,所以一般的项目接口文档中都会有一个退出接口api。

当前端向这个退出api发送请求时,响应头中的Set-Cookie一般会将登陆时设置的cookie(JSESSIONID)的expires属性设置成一个过期时间。这样浏览器解析这个Set-Cookie时就将JSESSIONID删除掉了。

那为什么不可以用前端的js删除cookie呢,这里就涉及到了session信息,当你登陆网站后,后端服务器将一个cookie返回给前端,并且会在后端数据库存储一个cookie,这两个cookie是相同的,每次退出后两个cookie都应该删除,这就需要前端向后端发送一个删除cookie的请求,服务器接受到请求后删除cookie,并在响应头中设置如下信息。

浏览器接受到以上信息,根据expires字段信息判断cookie过期(1970年就过期了),将cookie删除。这样两个cookie就都删除了。

如果只是用前端js将cookie手动清除,后端依然保存着cookie,造成资源浪费,当然还有一些其他的弊端,这里不做赘述。

localStorage 和 sessionStorage

Web Storage 包含如下两种机制:

  • sessionStorage 为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
  • localStorage 同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。

这两种机制是通过 Window.sessionStorage 和 Window.localStorage 属性使用(更确切的说,在支持的浏览器中 Window 对象实现了 WindowLocalStorage 和 WindowSessionStorage 对象并挂在其 localStorage 和 sessionStorage 属性下)—— 调用其中任一对象会创建 Storage 对象,通过 Storage 对象,可以设置、获取和移除数据项。对于每个源(origin)sessionStorage 和 localStorage 使用不同的 Storage 对象——独立运行和控制。

例如,在文档中调用 localStorage 将会返回一个 Storage 对象;调用 sessionStorage 返回一个不同的 Storage 对象。可以使用相同的方式操作这些对象,但是操作是独立的。

indexDB

ndexedDB是一种底层API,用于客户端存储大量结构化数据(包括, 文件/ 二进制大型对象(blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太好用。IndexedDB提供了一个解决方案。

IndexedDB是一个事务型数据库系统,类似于基于SQL的RDBMS。 然而,不像RDBMS使用固定列表,IndexedDB是一个基于JavaScript的面向对象数据库。 IndexedDB允许您存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。 您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务。

Service Worker

有一个困扰 web 用户多年的难题——丢失网络连接。即使是世界上最好的 web app,如果下载不了它,也是非常糟糕的体验。Service worker 最终要去解决这些问题。Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能。这是原生APP 本来就支持的功能,这也是相比于 web app,原生 app 更受青睐的主要原因。

下图展示了 service worker 所有支持的事件:

在这里插入图片描述

现在来谈谈 Service workers
注册你的 worker

service worker 的入口:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}
  1. 外面的代码块做了一个特性检查,在注册之前确保 service worker 是支持的。
  2. 接着,我们使用 ServiceWorkerContainer.register() 函数来注册 站点的 service worker,service worker 只是一个驻留在我们的 app 内的一个 JavaScript 文件 (注意,这个文件的url 是相对于 origin, 而不是相对于引用它的那个 JS 文件)。
  3. scope 参数是选填的,可以被用来指定你想让 service worker 控制的内容的子目录。 在这个例子例,我们指定了 ‘/sw-test/’,表示 app 的 origin 下的所有内容。如果你留空的话,默认值也是这个值, 我们在指定只是作为例子。
  4. .then() 函数链式调用我们的 promise,当 promise resolve 的时候,里面的代码就会执行。
  5. 最后面我们链了一个 .catch() 函数,当 promise rejected 才会执行。

这就注册了一个 service worker,它工作在 worker context,所以没有访问 DOM 的权限。在正常的页面之外运行 service worker 的代码来控制它们的加载。

单个 service worker 可以控制很多页面。每个你的 scope 里的页面加载完的时候,安装在页面的 service worker 可以控制它。牢记你需要小心 service worker 脚本里的全局变量: 每个页面不会有自己独有的worker。

注意: 你的 service worker 函数像一个代理服务器一样,允许你修改请求和响应,用他们的缓存替代它们等等。

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

安装和激活:填充你的缓存

在你的 service worker 注册之后,浏览器会尝试为你的页面或站点安装并激活它。

install 事件会在安装完成之后触发。install 事件一般是被用来填充你的浏览器的离线缓存能力。为了达成这个目的,我们使用了 Service Worker 的 新的标志性的存储 API — cache — 一个 service worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成key。这个 API 和浏览器的标准的缓存工作原理很相似,但是是特定你的域的。它会一直持久存在,直到你告诉它不再存储,你拥有全部的控制权。

让我们从一个代码示例来开始这个部分:

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});
  1. 这里我们 新增了一个 install 事件监听器,接着在事件上接了一个ExtendableEvent.waitUntil() 方法——这会确保Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成。
  2. 在 waitUntil() 内,我们使用了 caches.open() 方法来创建了一个叫做 v1 的新的缓存,将会是我们的站点资源缓存的第一个版本。它返回了一个创建缓存的 promise,当它 resolved的时候,我们接着会调用在创建的缓存示例上的一个方法 addAll(),这个方法的参数是一个由一组相对于 origin 的 URL 组成的数组,这些 URL 就是你想缓存的资源的列表。
  3. 如果 promise 被 rejected,安装就会失败,这个 worker 不会做任何事情。这也是可以的,因为你可以修复你的代码,在下次注册发生的时候,又可以进行尝试。
  4. 当安装成功完成之后, service worker 就会激活。在第一次你的 service worker 注册/激活时,这并不会有什么不同。但是当 service worker 更新的时候 ,就不太一样了。

注意: localStorage 跟 service worker 的 cache 工作原理很类似,但是它是同步的,所以不允许在 service workers 内使用。

注意: IndexedDB 可以在 service worker 内做数据存储。

自定义请求的响应

现在你已经将你的站点资源缓存了,你需要告诉 service worker 让它用这些缓存内容来做点什么。有了 fetch 事件,这是很容易做到的。

每次任何被 service worker 控制的资源被请求到时,都会触发 fetch 事件,这些资源包括了指定的 scope 内的文档,和这些文档内引用的其他任何资源(比如 index.html 发起了一个跨域的请求来嵌入一个图片,这个也会通过 service worker 。)

你可以给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后你用可以用自己的魔法来更新他们。

我们可以用一个简单的例子开始,在任何情况下我们只是简单的响应这些缓存中的 url 和网络请求匹配的资源。

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
  );
});

caches.match(event.request) 允许我们对网络请求的资源和 cache 里可获取的资源进行匹配,查看是否缓存中有相应的资源。这个匹配通过 url 和 vary header进行,就像正常的 http 请求一样。

让我们看看我们在定义我们的魔法时的一些其他的选项。

  1. 如果没有在缓存中找到匹配的资源,你可以告诉浏览器对着资源直接去fetch 默认的网络请求:fetch(event.request)
  2. 如果没有在缓存中找到匹配的资源,同时网络也不可用,你可以用 match() 把一些回退的页面作为响应来匹配这些资源,比如:caches.match('/fallback.html');
恢复失败的请求

在有 service worker cache 里匹配的资源时, caches.match(event.request) 是非常棒的。但是如果没有匹配资源呢?如果我们不提供任何错误处理,promise 就会 reject,同时也会出现一个网络错误。

幸运的是,service worker 的基于 promise 的结构,使得提供更多的成功的选项变得微不足道。 我们可以这样做:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

如果 promise reject了, catch() 函数会执行默认的网络请求,意味着在网络可用的时候可以直接像服务器请求资源。

如果我们足够聪明的话,我们就不会只是从服务器请求资源,而且还会把请求到的资源保存到缓存中,以便将来离线时所用!这意味着如果其他额外的图片被加入到 Star Wars 图库里,我们的 app 会自动抓取它们。下面就是这个诀窍:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(resp) {
      return resp || fetch(event.request).then(function(response) {
        return caches.open('v1').then(function(cache) {
          cache.put(event.request, response.clone());
          return response;
        });  
      });
    })
  );
});

这里我们用 fetch(event.request) 返回了默认的网络请求,它返回了一个 promise 。当网络请求的 promise 成功的时候,我们 通过执行一个函数用 caches.open(‘v1’) 来抓取我们的缓存,它也返回了一个 promise。当这个 promise 成功的时候, cache.put() 被用来把这些资源加入缓存中。资源是从 event.request 抓取的,它的响应会被 response.clone() 克隆一份然后被加入缓存。这个克隆被放到缓存中,它的原始响应则会返回给浏览器来给调用它的页面。

为什么要这样做?这是因为请求和响应流只能被读取一次。为了给浏览器返回响应以及把它缓存起来,我们不得不克隆一份。所以原始的会返回给浏览器,克隆的会发送到缓存中。它们都是读取了一次。

我们现在唯一的问题是当请求没有匹配到缓存中的任何资源的时候,以及网络不可用的时候,我们的请求依然会失败。让我们提供一个默认的回退方案以便不管发生了什么,用户至少能得到些东西:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function() {
      return fetch(event.request).then(function(response) {
        return caches.open('v1').then(function(cache) {
          cache.put(event.request, response.clone());
          return response;
        });  
      });
    }).catch(function() {
      return caches.match('/sw-test/gallery/myLittleVader.jpg');
    })
  );
});

因为只有新图片会失败,我们已经选择了回退的图片,一切都依赖我们之前看到的 install 事件侦听器中的安装过程。

更新你的 service worker

如果你的 service worker 已经被安装,但是刷新页面时有一个新版本的可用,新版的 service worker 会在后台安装,但是还没激活。当不再有任何已加载的页面在使用旧版的 service worker 的时候,新版本才会激活。一旦再也没有更多的这样已加载的页面,新的 service worker 就会被激活。

你想把你的新版的 service worker 里的 install 事件监听器改成下面这样(注意新的版本号):

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v2').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',// include other new resources for the new version...
      ]);
    })
  );
});

安装发生的时候,前一个版本依然在响应请求,新的版本正在后台安装,我们调用了一个新的缓存 v2,所以前一个 v1 版本的缓存不会被扰乱。

当没有页面在使用当前的版本的时候,这个新的 service worker 就会激活并开始响应请求。

删除旧缓存

你还有个 activate 事件。当之前版本还在运行的时候,一般被用来做些会破坏它的事情,比如摆脱旧版的缓存。在避免占满太多磁盘空间清理一些不再需要的数据的时候也是非常有用的,每个浏览器都对 service worker 可以用的缓存空间有个硬性的限制。浏览器尽力管理磁盘空间,但它可能会删除整个域的缓存。浏览器通常会删除域下面的所有的数据。

传给 waitUntil() 的 promise 会阻塞其他的事件,直到它完成。所以你可以确保你的清理操作会在你的的第一次 fetch 事件之前会完成。

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['v2'];

  event.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (cacheWhitelist.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  );
});
一个简单完整的例子

ervice Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
在这里插入图片描述
在 Cache 中也可以发现我们所需的文件已被缓存
在这里插入图片描述

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的
在这里插入图片描述
到此结束!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值