可能不是史上最全但肯定能学会的浏览器存储教程

简介: 可能不是史上最全但肯定能学会的浏览器存储教程

对于一个网站而言,重要数据通常都要存储到服务器的数据库中。

但有时为了方便,我们也会将一些数据缓存到浏览器中。比如会话状态管理(包括用户认证令牌、账号信息、购物车等)、个性化设置(包括主题配置、语言配置等)和用户行为跟踪等信息。

这种做法可以减少不必要的请求,即提高了前端应用的性能,又可以降低服务器的压力。

浏览器存储技术有非常多,包含 Cookie、Web Storage(Local Storage、Session Storage)、IndexedDB、Web SQL、Trust Tokens、Cache Storage 和 Application Cache。

这一篇文章主要对最常用的 Cookie、WebStorage 和 IndexedDB 三种存储技术进行讲解。


Cookie


Cookie 曾经是最流行的客户端存储技术。

由于 http 是无状态的,我们无法得知两个请求是否来自于同一个用户,以及这个用户当前的状态。比如你点击了购买按钮,发送了一个 http 请求,服务器可能需要知道你是哪个用户,你的账号下有多少余额、多少优惠券、绑定了哪些银行卡、收货地址是否完善等。所以 cookie 的根本目的是为了帮助 http 维护状态

针对这类场景,浏览器推出 cookie 这种方案来记录这些信息,具体的规范在 RFC6265 中。

不过随着浏览器存储技术的发展,更多开发者选择使用 Local Storage 配合 JWT 来实现用户状态存储,这似乎是一种更好的方式。但我们仍然有必要了解 Cookie,因为仍然有大量项目是使用 Cookie 开发的。

Cookie 的特点:

  • 绝大多数存储 API 都属于 BOM,只有 Cookie 属于 DOM。

Cookie 的缺点:

  • 存在大小限制,通常是 4 K。不过这是对单个 cookie 的 value 限制,并不是对所有的 cookie。
  • 体积过大,对性能不好。由于 cookie 是在同域名下所有请求自动附带,很多静态资源,如 js、css、png 等文件也会附带 cookie。
  • 会自动发送到服务器,无法人工取消。
  • API 设计不优雅,难以操作。


Cookie 的使用

设置 Cookie


cookie 可以双向传递,可以由服务器设置 cookie,传递给浏览器。也可以由浏览器设置,传递给服务器。


服务器设置 Cookie Set-Cookie


在服务器设置 cookie,只需要在请求头中设置 Set-Cookie 字段,值为 cookie_name=cookie_value 的形式,多个 cookie 之间用 ; 分隔。

可以查看下面使用 express 做的例子。

server.js


var express = require("express");
const path = require("path");
var app = express();
app.get("/", function (req, res) {
  res.setHeader('Set-Cookie', ['userID=123456',
    'lang=zh-CN'])
  res.sendFile(path.resolve(__dirname, './index.html'))
})
app.listen(3000);

在服务器的请求头中设置了 userID 和 lang 两个 cookie 字段。

index.js


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cookie</title>
  </head>
  <body>
    <div id="info"></div>
    <script>
      const infoEl = document.getElementById('info')
      infoEl.innerHTML = document.cookie;
    </script>
  </body>
</html>

浏览器会自动将 http 响应头中的 Set-Cookie 设置到 document.cookie 中,这是浏览器的默认行为。


浏览器设置 Cookie


使用 JS 设置 Cookie 非常简单,只需要将 document.cookie = xx 就可以。

添加一个接口。


app.get("/cookie", function (req, res) {
  console.log(req.headers.cookie);
  res.send("")
})

添加一个按钮。


<button id="btn">发送请求</button>

添加设置 cookie 并请求接口的代码。


const btnEl = document.getElementById('btn')
btnEl.addEventListener('click',()=>{
  document.cookie = "userID=555555"
  fetch('http://localhost:3000/cookie')
});

这样就可以修改 cookie 的值再传递给服务器。

浏览器中存在 cookie,都会默认附加在请求头的 cookie 字段中。这也是浏览器的默认行为。


生命周期


所有存储在浏览器的数据,本质上都是缓存数据,都会按照某种机制删除或者更新。

Cookie 大体上可以分为两类,会话期 Cookie 和持久性 Cookie。

会话期 Cookie 会在浏览器关闭后自动删除。

持久性 Cookie 需要设置过期时间(Expires)或者有效期(Max-Age)。

这个生命周期只会对浏览器有作用,包括过期时间和有效期都是根据浏览器的时间来计算的。

通常绝大多数情况下,后端框架大都会封装一些方便的的 API,比如在 express 中设置过期时间和有效期可以这么写。


app.get("/", function (req, res) {
  res.cookie("userID", "123456", {
    maxAge: 2 * 1000,
    expires: Date.now() + 1000,
  });
  res.sendFile(path.resolve(__dirname, "./index.html"));
});

无论用什么样的 API 来完成 cookie 的设置,总之最终的效果是要在 http 的 response headers 中添加一个 Set-Cookie 字段,如下所示:

image.png

这样就表示 userID 这个 cookie 字段的有效期是 2 秒,过期时间是当前时间 + 1秒。

当这两个字段都被设置后,相对较近的那个时间会生效。

最后可以在 index.html 中添加这段脚本来查看过期后的效果。


console.log(document.cookie);
setTimeout(() => {
  console.log(document.cookie);
}, 1100);


作用域

domain


默认情况下,cookie 只能在 origin 下使用,并不包含子域名。

比如默认情况下 www.a.com 可以使用,但 b.c.com 就不能使用。

如果要和子域名共享 cookie,需要设置 Domain 属性。比如 Domain=a.com,这样 *.a.com 都可以共享 cookie。


path


类似的,我们还可以指定哪些路径可以接受 cookie。控制路径的属性是 path,比如 path=/docs。


sameSite


sameSite 属性允许服务器要求某个 cookie 字段在跨站请求时不会被发送,从而可以阻止 CSRF 攻击。

  • None:浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。
  • Strict:浏览器将只在访问相同站点时发送 cookie。(在原有 Cookies 的限制条件上的加强,如上文 “Cookie 的作用域” 所述)
  • Lax:与 Strict 类似,但用户从外部站点导航至URL时(例如通过链接)除外。 在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到URL时才会发送。如 link 链接。


安全性

secure


当设置了 secure 后,该 cookie 字段就不能被应用于 http 协议的请求,只能应用于 https 的请求。


http-only


标记了 http-only 后,该 cookie 字段就不能被 document.cookie 访问到了。一些比较重要的信息应该使用这个标记。http only 可以很好的缓解 XSS 攻击。


删除 cookie


JavaScript 是无法直接删除 cookie 的,必须使用一种奇怪的方式来删除 cookie,就是将 cookie 的过期时间调整至一个很早之前的时间。

比如要删除 foo,要像下面这么写。


document.cookie = "foo=;expires=Thu, 01 Jan 1970 00:00:01 GMT;"

js cookie


由于在浏览器操作 cookie 的方式并不友好,可以使用一些对 cookie 操作进行封装,提供了更加优雅的 API 的库,比如 js-cookie

使用 js-cookie 库后,操作 cookie 的方式如下所示:


Cookies.set('foo', 'bar')// 设置
Cookies.get('foo')// 获取
Cookies.remove('foo')// 移除


cookieStorage


Chrome 87 版本后,支持了 cookieStorage API。

这个 API 可以更好的操作 cookie。


cookieStorage.set("foo", "bar")
cookieStorage.get("foo")
cookieStorage.delete("foo")

cookieStorage 除了提供上述三个 API 外,还提供了 change 事件,可以监听 change 事件来响应 cookie 的变化。


Session Cookie 模型


session cookie 是一个经典的存储用户状态的模型。

具体的工作流程是:服务器为每一个用户创建一个 session,通常会存储到服务器的内存中。每个用户在认证后会得到一个 session id,存储到 cookie 中。之后每次来自用户的请求,都会在请求头中取出 cookie,去和 session 进行查询,如果这个 session 存在,就会取出数据用于业务逻辑处理。

下面是使用 express 实现的 session 示例。


var express = require("express");
var cookies = require("cookie-parser");
var app = express();
app.use(cookies());
const session = new Map();
function genid() {
  return Math.random();
}
app.get("/", function (req, res, next) {
  if (req.cookies && "sid" in req.cookies && session.has(+req.cookies.sid)) {
    res.setHeader("Content-Type", "text/html;charset=utf-8");
    res.write("<p>您已登录</p>");
    res.end();
  } else {
    var sid = genid();
    res.cookie("sid", sid);
    session.set(sid, {});
    res.end("welcome to the session demo. refresh!");
  }
});
app.listen(3011);

实现 session,需要自己生成 session id,自己维护 session,比较麻烦。通常我们会使用一些库可以帮助我们做这种事情,比如 express-session。


var express = require("express");
const session = require("express-session");
var app = express();
app.use(
  session({
    secret: "keyboard cat",
    cookie: { maxAge: 60000 },
  })
);
app.get("/", function (req, res, next) {
  if (req.session.views) {
    req.session.views++;
    res.setHeader("Content-Type", "text/html");
    res.write("<p>views: " + req.session.views + "</p>");
    res.write("<p>expires in: " + req.session.cookie.maxAge / 1000 + "s</p>");
    res.end();
  } else {
    req.session.views = 1;
    res.end("welcome to the session demo. refresh!");
  }
});
app.listen(3011);

优缺点


下面是对 cookie session 模型的优缺点分析:

优点:

  • 浏览器默认技术
  • 简单的键值对存储结构
  • 支持过期自动清理
  • 默认自动携带

缺点:

  • 不能跨域名
  • 不能跨浏览器
  • 难以实现单点登录


URL 重写


有些用户或者某些浏览器会禁用 cookie,这时可以通过 url 重写来传递 cookie。

比如下面这个请求。

image.png

禁用 cookie 后,改为 http://localhost:3011/?sid=0.9382480676608358


WebStorage


web storage 是 HTML5 中推出的新存储方案,是对 cookie 的一种改善。

web storage 分为两类,一类是会话级别的 sessionStorage,浏览器关闭后,就会自动清除,用于临时存储。

另一类是 localStorage,将数据持久化的存储到硬件设备上,即使浏览器关闭也不会清除,用于永久保存。

两者的区别仅在于数据的储存时间,其他没有任何区别。

和 cookie 相比,WebStorage 做出了以下改善:

  • 容量提高,最大容量为 10 MB。不同的浏览器大小不同,大概 2.5MB 到 10MB 之间。
  • 和通信无关,不会自动添加到 http 的 request headers 中。
  • 更加友好的 API。

虽然 WebStorage 做出了很多改善,但如果遭受到 XSS 攻击,WebStorage 没有 http only 之类的保护策略,所以数据更容易被窃取。在安全方面,WebStorage 并不会比 Cookie 做的更好。


基本用法


session storage 的 API 很简洁,只有 setItem、getItem、removeItem、key 和 clear 5 个方法,操作如下:


sessionStorage.setItem('key', 'value')// 设置一个键值对,第一个参数是键,第二个参数是值
sessionStorage.getItem('key')// 获取名为 key 的值
sessionStorage.removeItem('key')// 移除名为 key 的值
sessionStorage.key(0)// 获取存储中第 n 个键名
sessionStorage.clear()// 全部清除

两者的 API 是完全一致的。所以 local storage 对应的 API 如下:


localStorage.setItem('key', 'value')// 设置一个键值对,第一个参数是键,第二个参数是值
localStorage.getItem('key')// 获取名为 key 的值
localStorage.removeItem('key')// 移除名为 key 的值
localStorage.key(0)// 获取存储中第 n 个键名
localStorage.clear()// 全部清除

除了上述 API 操作以外,还可以通过对象的方式操作它们。


sessionStorage.key = 'value'// 设置一个键值对,第一个参数是键,第二个参数是值
sessionStorage.key// 获取名为 key 的值
delete sessionStorage.key// 移除名为 key 的值

跨页面监听事件


在一些复杂场景下,我们可能需要在两个页面中共同访问 storage 的变化,从而处理一些事情,所以需要对 storage 的变化进行监听。

WebStorage 提供了 storage 事件。

在 page1.html 中监听事件。


<!DOCTYPE html>
<html>
  <body>
    <script>
      window.addEventListener("storage", (e) => {
        console.log(e);
      });
    </script>
  </body>
</html>

在 page2.html 中存储数据。


<!DOCTYPE html>
<html>
  <body>
    <input type="text" />
    <button onclick="save()">存储</button>
    <script>
      function save() {
        localStorage.setItem(
          "value",
          document.getElementsByTagName("input")[0].value
        );
      }
    </script>
  </body>
</html>

storage 的 event 中包含了很多有用的数据。

  • key:表示修改了哪个键。
  • oldValue:表示修改前的旧值。
  • newValue:表示修改后的新值。
  • target:表示修改数据的文档的 Window 对象。
  • url:表示修改数据的文档的 URL。
  • type:表示什么存储类型。
  • storageArea:表示存储的对象,指向 sessionStorage 或者 localStorage。

需要注意:storage 事件不能响应当前页面的操作。


WebStorage 配合 JWT 实现用户认证


很多人从 cookie 切换到 WebStorage 面临的第一个问题估计就是用户认证。

现在流行使用另外一种存储用户信息的技术:JWT。JWT 可以替代 Session。


JWT


JWT 是 JSON Web Token 的缩写,官方规范在 RFC7519

JWT 是一种开放标准,用于在两方(客户端与服务端)之间共享安全信息。每个 JWT 都是 2 个编码后的 JSON 对象。JWT 可以选择不同的加密算法对 JSON 对象加密,以确保生成后的 JWT 字符串不能被随意篡改。

比如下面这段 JSON:


{
  "user_id": "1234567890",
  "user_name": "李明"
}

通过 HS256 加密算法进行加密。


{
  "alg": "HS256",
  "typ": "JWT"
}

最终生成的就是一段字符串文本。


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsInVzZXJfbmFtZSI6IuadjuaYjiJ9.4bMWJsnfg0G1uH5qJvnxbZCfFjOL8E4uDZ5Khnr00iI

JWT 字符串由 3 部分组成,每个部分由 . 分隔。看起来就像是 xxxxx.yyyyy.zzzzz

它们分别可以解析出 3 个 JSON 对象,分别是标头(Header)、有效负载(Payload)和签名(Verify Signature)。

标头主要包含签名算法类型,比如下面这样:


{
  "alg": "HS256",
  "typ": "JWT"
}

有效负载的内容就是由你自己定义的了。但通常不会包含太多信息,以保证 JWT 的紧凑型。

最后的签名可以自己指定一个密钥(secret)。


JWT 的优势


使用 cookie session 模型,用户的认证信息由服务端存储。

使用 JWT,用户的认证信息就交由客户端存储,节省了服务端的存储空间,更重要的是这是一种无状态的认证方式,更容易实现单点登录。

和 session 相比,唯一的缺点只不过每一次都需要在服务端进行解析。不过这也是一种利用性能换取空间的常规代价。


加密


当我们获取到 JWT 令牌后,还需要将它存储到某个位置,这时我们通常会根据用户选择的记忆模式来选择是使用 LocalStorage 还是 SessionStorage。比如用户勾选了记住密码,我们就会将 JWT 存储到 LocaStorage 中,否则会存储到 SessionStorage 中。

虽然 JWT 官方不推荐我们将敏感信息存到 WebStorage 中,而是存到 Cookie 中,因为 Cookie 拥有 http only 的保护。但考虑到 WebStorage API 的便利性,很多人还是更加愿意将数据存入 WebStorage

为了保护数据,我们应该始终对 WebStorage 中的数据进行校验和加密。这样在一定程度上可以提高安全性,降低被攻击后的后果。

有一些库可以做这件事情,比如 secure-ls

不过有可能的话,我个人更加建议将 JWT 放置在 Cookie 中,并且设置 http only。


Authorization


http 请求头中有一个 Authorization 字段,用于发送用户身份凭证。

我们生成的 JWT 通常会放在这里。

它的格式如下:


Authorization: <type> <credentials>

其中 Type 可以设置很多种值。比如基础的 Basic、IANAAmazon 中的验证方案。

在 Authorization 中设置 JWT 大概是下面这样:


Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsInVzZXJfbmFtZSI6IuadjuaYjiJ9.4bMWJsnfg0G1uH5qJvnxbZCfFjOL8E4uDZ5Khnr00iI

Demo


很多编程语言都提供了 JWT 的库,比如在 Nodejs 中就有 jsonwebtoken。下面是一个 JWT 的程序示例。

server.js


var express = require("express");
var bodyParser = require("body-parser");
const path = require("path");
var jwt = require("jsonwebtoken");
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.get("/", function (req, res, next) {
  res.sendFile(path.resolve(__dirname, "./index.html"));
});
app.post("/login", function (req, res) {
  var token = jwt.sign({ username: req.body.username }, "shhhhh");
  res.send({ token });
});
app.listen(3011);

index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <input id="username" type="text" />
    <input id="password" type="password" />
    <input id="btn" value="登陆" type="submit" />
    <script>
      document.getElementById("btn").addEventListener("click", () => {
        const username = document.getElementById("username").value;
        const password = document.getElementById("password").value;
        fetch("./login", {
          method: "post",
          body: JSON.stringify({ username, password }),
          headers: { "Content-Type": "application/json" },
        })
          .then((res) => res.json())
          .then((data) => {
            document.writeln(JSON.stringify(data));
          });
      });
    </script>
  </body>
</html>

IndexedDB


indexedDB 是一种更加低级的 API,用于存储大量数据。

WebStorage 的优势很明显,它很轻量,但缺乏搜索能力。当我们的数据过多时,需要使用搜索能力,就需要一种新的解决方案,那就是 indexedDB。


本地数据库概念


通常来说,数据库的概念一般用于后端,前端工程师很少有数据库的概念,而且很多前端工程师连数据模型的概念都很模糊。所以要学习 indexedDB 可能需要接触和理解很多概念。


特性

键值对存储


indexedDB 是一种非结构化存储,更加接近 MongoDB 这种数据库,而不是像 MySQL 这类数据库。


主键


每一个数据都需要有一个独一无二的字段作为主键,主键不可重复,否则会抛出错误。


异步


indexedDB 的所有操作都是异步的,这不同于 WebStorage。异步设计是为了防止阻塞主线程。


事务


当同时执行多个操作时,只要有一个操作失败,所有操作全部回滚到操作之前的状态,这样可以保证数据一致性,不会产生只修改了部分数据、数据不一致的情况。


同源限制


每个网页只能访问该域名下的数据库,不能跨域访问数据库。


支持索引


支持建立索引,可以大幅提高搜索效率。


支持二进制存储


IndexedDB 支持存储 ArrayBuffer、Blob 等二进制类型数据。


无限制的存储空间


IndexedDB 不会低于 250MB,但理论上没有最大上限。


对象


IndexedDB 是一组较为复杂的 API,它一共提供了 7 个 API:

  • 数据库:IDBDatabase 对象。
  • 仓库对象:IDBObjectStore 对象。
  • 索引:IDBIndex 对象。
  • 事务:IDBTransaction 对象。
  • 操作:IDBRequest 对象。
  • 指针:IDBCursor 对象。
  • 主键:IDBKeyRange 对象。

关于它们之间的详细用法,在下面 API 的实际操作中再来讲解。


API 操作

创建数据库&连接数据库


通过 indexedDB.open 方法连接数据库。如果不存在这个数据库,则会创建一个新的数据库。

该方法接收两个参数,分别是数据库名和数据库版本号。它会返回一个 IDBRequest 实例。

IDBRequest 实例有 onsuccess 事件和 onupgradeneeded 事件。

onsuccess 事件是数据库成功打开时触发的,这时 IDBRequest 实例上的 result 属性就是 IDBDatabase 对象。

onupgradeneeded 事件是数据库的版本大于之前版本的时候触发。

onupgradeneeded 事件会早于 onsuccess 事件执行。


let db;
let DBRequestLink = window.indexedDB.open('dataBaseName', 1)
DBRequestLink.onsuccess = function(event) {
  console.log('success');
  db = DBRequestLink.result;
};
DBRequestLink.onupgradeneeded = function(event) {
  console.log('upgradeneeded');
};

创建表&创建索引


创建表通过 IDBDatabase 实例的 createObjectStore 方法来创建。

该方法需要两个参数,第一个是表名,第二个是配置对象。在配置对象中可以设置主键名称和自增等等。

createObjectStore 方法会返回一个 IDBObjectStore 实例。

通过 IDBObjectStore 实例可以创建索引,方法是 createIndex,参数有三个。前两个分别是索引名和对象属性名,第三个是可选参数,表示对象,可以指定索引是否唯一。


DBOpenRequest.onupgradeneeded = function(event) {
  let db = event.target.result;
  let objectStore = db.createObjectStore('person', {
    keyPath: 'id',
    autoIncrement: true
  });
  objectStore.createIndex('id', 'id', {
    unique: true
  });
  objectStore.createIndex('name', 'name');
  objectStore.createIndex('age', 'age');
  objectStore.createIndex('sex', 'sex');
};


增加数据


每条数据就是一条 JavaScript 对象,属性和表的字段一一对应。

在存储数据之前要先获取 IDBTransaction 实例对象。通过 IDBDatabase 对象的 transaction 方法获取。该方法最多可接收三个参数,第一个参数是数据库名称,第二个参数是事务操作模式,可选,共三种:readonly、readwrite 和 readwriteflush,默认是 readonly。第三个参数是配置对象,可选。

transaction 方法返回的 IDBTransaction 对象有一个 objectStore 方法用于获取表,参数是表的名称,返回值是 IDBObjectStore 对象。

IDBObjectStore 对象的 add 方法用于添加数据。


let newItem = {
  id: 1,
  name: '张三',
  age: 3,
  sex: 'female'
};
let transaction = db.transaction('dataBaseName', "readwrite");
let objectStore = transaction.objectStore('person');
objectStore.add(newItem);

查询数据


使用 get 方法,传入 id 进行查询。


let transaction = db.transaction('dataBaseName', "readwrite");
let objectStore = transaction.objectStore('person');
let objectStoreRequest = objectStore.get(1);

修改数据


我们要进行修改数据,首先要有修改后的对象和被修改数据的 id。同样需要得到 IDBObjectStore 对象。通过 get 方法去查询数据,查询到结果后会触发 success 方法,result 属性就是那个对象,对这个对象的属性进行替换,最后把新的对象 put 到表中,完成数据的修改。


let transaction = db.transaction('dataBaseName', "readwrite");
let newRecord = {
  id: 1,
  name: '李四',
  age: 5,
  sex: 'male'
};
let objectStore = transaction.objectStore('person');
let objectStoreRequest = objectStore.get(1);
objectStoreRequest.onsuccess = function(event) {
  var record = objectStoreRequest.result;
  for (let key in newRecord) {
    if (typeof record[key] != 'undefined' || key !== 'id') {
      record[key] = newRecord[key];
    }
  }
  objectStore.put(record);
};

删除数据


删除数据比较简单,通过调用  IDBObject.delete 方法,传入 id,即可删除。


let transaction = db.transaction('dataBaseName', "readwrite");
let objectStore = transaction.objectStore('person');
let objectStoreRequest = objectStore.delete(1);

使用 IndexedDB 开发 TODOList


为了学会更灵活的使用 IndexedDB,下面是使用 IndexedDB 开发的一个 TODOList 程序。你可以参照这个代码自己实现一个 TODOList。

image.png

源码:


<style>
  * {
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
  }
  body,
  html {
    padding: 0;
    margin: 0;
  }
  body {
    font-family: Helvetica, Arial, sans-serif;
    color: #545454;
    background: #f7f7f7;
  }
  #page-wrapper {
    width: 550px;
    margin: 2.5em auto;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
    border-radius: 3px;
  }
  #new-todo-form {
    padding: 0.5em;
    background: pink;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
  }
  #new-todo {
    width: 100%;
    padding: 0.5em;
    font-size: 1em;
    border-radius: 3px;
    border: 0;
    outline: none;
  }
  #todo-items {
    list-style: none;
    padding: 0.5em 1em;
    margin: 0;
  }
  #todo-items li {
    font-size: 0.9em;
    padding: 0.5em;
    background: #fff;
    border-bottom: 1px solid #eee;
    margin: 0.5em 0;
  }
  input[type="checkbox"] {
    margin-right: 10px;
  }
</style>
<div id="page-wrapper">
  <!-- Form for new Todo Items -->
  <form id="new-todo-form" method="POST" action="#">
    <input
      type="text"
      name="new-todo"
      id="new-todo"
      placeholder="Enter a todo item..."
      required
    />
  </form>
  <!-- Todo Item List -->
  <ul id="todo-items"></ul>
</div>
<script>
  var todoDB = (function () {
    var tDB = {};
    var datastore = null;
    /**
     * Open a connection to the datastore.
     */
    tDB.open = function (callback) {
      // Database version.
      var version = 1;
      // Open a connection to the datastore.
      var request = indexedDB.open("todos", version);
      // Handle datastore upgrades.
      request.onupgradeneeded = function (e) {
        var db = e.target.result;
        e.target.transaction.onerror = tDB.onerror;
        // Delete the old datastore.
        if (db.objectStoreNames.contains("todo")) {
          db.deleteObjectStore("todo");
        }
        // Create a new datastore.
        var store = db.createObjectStore("todo", {
          keyPath: "timestamp",
        });
      };
      // Handle successful datastore access.
      request.onsuccess = function (e) {
        // Get a reference to the DB.
        datastore = e.target.result;
        // Execute the callback.
        callback();
      };
      // Handle errors when opening the datastore.
      request.onerror = tDB.onerror;
    };
    /**
     * Fetch all of the todo items in the datastore.
     * @param {function} callback A function that will be executed once the items
     *                            have been retrieved. Will be passed a param with
     *                            an array of the todo items.
     */
    tDB.fetchTodos = function (callback) {
      var db = datastore;
      var transaction = db.transaction(["todo"], "readwrite");
      var objStore = transaction.objectStore("todo");
      var keyRange = IDBKeyRange.lowerBound(0);
      var cursorRequest = objStore.openCursor(keyRange);
      var todos = [];
      transaction.oncomplete = function (e) {
        // Execute the callback function.
        callback(todos);
      };
      cursorRequest.onsuccess = function (e) {
        var result = e.target.result;
        if (!!result == false) {
          return;
        }
        todos.push(result.value);
        result.continue();
      };
      cursorRequest.onerror = tDB.onerror;
    };
    /**
     * Create a new todo item.
     * @param {string} text The todo item.
     */
    tDB.createTodo = function (text, callback) {
      // Get a reference to the db.
      var db = datastore;
      // Initiate a new transaction.
      var transaction = db.transaction(["todo"], "readwrite");
      // Get the datastore.
      var objStore = transaction.objectStore("todo");
      // Create a timestamp for the todo item.
      var timestamp = new Date().getTime();
      // Create an object for the todo item.
      var todo = {
        text: text,
        timestamp: timestamp,
      };
      // Create the datastore request.
      var request = objStore.put(todo);
      // Handle a successful datastore put.
      request.onsuccess = function (e) {
        // Execute the callback function.
        callback(todo);
      };
      // Handle errors.
      request.onerror = tDB.onerror;
    };
    /**
     * Delete a todo item.
     * @param {int} id The timestamp (id) of the todo item to be deleted.
     * @param {function} callback A callback function that will be executed if the
     *                            delete is successful.
     */
    tDB.deleteTodo = function (id, callback) {
      var db = datastore;
      var transaction = db.transaction(["todo"], "readwrite");
      var objStore = transaction.objectStore("todo");
      var request = objStore.delete(id);
      request.onsuccess = function (e) {
        callback();
      };
      request.onerror = function (e) {
        console.log(e);
      };
    };
    // Export the tDB object.
    return tDB;
  })();
  window.onload = function () {
    // Display the todo items.
    todoDB.open(refreshTodos);
    // Get references to the form elements.
    var newTodoForm = document.getElementById("new-todo-form");
    var newTodoInput = document.getElementById("new-todo");
    // Handle new todo item form submissions.
    newTodoForm.onsubmit = function () {
      // Get the todo text.
      var text = newTodoInput.value;
      // Check to make sure the text is not blank (or just spaces).
      if (text.replace(/ /g, "") != "") {
        // Create the todo item.
        todoDB.createTodo(text, function (todo) {
          refreshTodos();
        });
      }
      // Reset the input field.
      newTodoInput.value = "";
      // Don't send the form.
      return false;
    };
  };
  function refreshTodos() {
    todoDB.fetchTodos(function (todos) {
      var todoList = document.getElementById("todo-items");
      todoList.innerHTML = "";
      for (var i = 0; i < todos.length; i++) {
        // Read the todo items backwards (most recent first).
        var todo = todos[todos.length - 1 - i];
        var li = document.createElement("li");
        var checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.className = "todo-checkbox";
        checkbox.setAttribute("data-id", todo.timestamp);
        li.appendChild(checkbox);
        var span = document.createElement("span");
        span.innerHTML = todo.text;
        li.appendChild(span);
        todoList.appendChild(li);
        // Setup an event listener for the checkbox.
        checkbox.addEventListener("click", function (e) {
          var id = parseInt(e.target.getAttribute("data-id"));
          todoDB.deleteTodo(id, refreshTodos);
        });
      }
    });
  }
</script>



由于 indexedDB 提供的 API 很低级,在实际的开发中使用较为困难,我们可以使用一些别人封装好的库来降低我们的心智负担,提高开发效率,比如 localforagedexie

尤其是 localforage,它有很多优势,我们可以指定存储方案,默认顺序为 IndexedDB、WebSQL、LocalStorage。如果浏览器不支持 IndexedDB,会自动降级。并且它的 API 非常简洁优雅,远没有 IndexedDB 那么复杂。


总结


如果是新项目,建议优先使用 WebStorage。

如果场景特殊,需要存储大量结构化数据,建议优先使用一些基于 IndexedDB 封装的库 localforagedexielevel-js

如果需要存储敏感数据,建议优先使用 Cookie。



相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
2月前
|
Web App开发 Java 测试技术
《手把手教你》系列基础篇之(四)-java+ selenium自动化测试- 启动三大浏览器(下)基于Maven(详细教程)
【2月更文挑战第13天】《手把手教你》系列基础篇之(四)-java+ selenium自动化测试- 启动三大浏览器(下)基于Maven(详细教程) 上一篇文章,宏哥已经在搭建的java项目环境中实践了,今天就在基于maven项目的环境中给小伙伴们 或者童鞋们演示一下。
71 1
|
2月前
|
Web App开发 Java 测试技术
《手把手教你》系列基础篇之(三)-java+ selenium自动化测试- 启动三大浏览器(上)(详细教程)
【2月更文挑战第12天】《手把手教你》系列基础篇之(三)-java+ selenium自动化测试- 启动三大浏览器(上)(详细教程) 前边宏哥已经将环境搭建好了,今天就在Java项目搭建环境中简单地实践一下: 启动三大浏览器。按市场份额来说,全球前三大浏览器是:IE.Firefox.Chrome。因此宏哥这里主要介绍一下如何启动这三大浏览器即可,其他浏览器类似的方法,照猫画虎就可以了。
49 1
|
7月前
|
存储 移动开发 安全
Spartacus cart id 存储在浏览器 local storage 里面
Spartacus cart id 存储在浏览器 local storage 里面
41 0
|
24天前
|
Java 测试技术 定位技术
《手把手教你》系列技巧篇(二十三)-java+ selenium自动化测试-webdriver处理浏览器多窗口切换下卷(详细教程)
【4月更文挑战第15天】本文介绍了如何使用Selenium进行浏览器窗口切换以操作不同页面元素。首先,获取浏览器窗口句柄有两种方法:获取所有窗口句柄的集合和获取当前窗口句柄。然后,通过`switchTo().window()`方法切换到目标窗口句柄。在项目实战部分,给出了一个示例,展示了在百度首页、新闻页面和地图页面之间切换并输入文字的操作。最后,文章还探讨了在某些情况下可能出现的问题,并提供了一个简单的本地HTML页面示例来演示窗口切换的正确操作。
44 0
|
1月前
|
存储 JavaScript 前端开发
在浏览器中存储数组和对象(js的问题)
在浏览器中存储数组和对象(js的问题)
12 0
|
7月前
|
Web App开发 搜索推荐 NoSQL
如何搭建一个集成导航与在线工具的个性化浏览器私有书签(附详细搭建教程)
在这个信息爆炸的时代,我们都希望拥有一个能够轻松解决多端、多浏览器的收藏和笔记同步问题的神奇工具。Mtab书签正是为此而设计的顶级应用。它将基础导航、记事本、在线小工具和多端同步集于一身,为用户提供了更便利的网络浏览体验,并解决了多端同步的烦恼。
173 0
如何搭建一个集成导航与在线工具的个性化浏览器私有书签(附详细搭建教程)
|
8月前
|
Web App开发 安全
【教程】谷歌浏览器移到其他盘之后,本地网页代码无法用谷歌浏览器打开的解决办法
【教程】谷歌浏览器移到其他盘之后,本地网页代码无法用谷歌浏览器打开的解决办法
|
8月前
|
Web App开发
[教程]谷歌浏览器只能安装在C盘,教大家如何设置才能装在D盘
[教程]谷歌浏览器只能安装在C盘,教大家如何设置才能装在D盘
|
4月前
|
测试技术 Python
python使用selenium操作浏览器的教程
python使用selenium操作浏览器的教程
68 1
python使用selenium操作浏览器的教程
|
8月前
|
存储 开发工具 数据安全/隐私保护
关于微软 Edge 浏览器无法访问笔者 SAP UI5 教程示例代码的问题
关于微软 Edge 浏览器无法访问笔者 SAP UI5 教程示例代码的问题
47 0

热门文章

最新文章