前端面试常备05:浏览器存储

浏览器本地存储随着技术的发展越来越强大, 主流的主要是:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB
  • webSQL(逐渐放弃)

Cookie

cookie 是一种纯文本的数据存储

工作过程

当网页要发送 http 请求的时候, 浏览器会先检查是否有相应的 cookie, 有则自动添加到 request header 中的 cookie 字段中.

存储在 cookie 中的数据, 每次都会被自动添加到 http 请求中, 必然是增加网络开销的.

Cookie 特征

  1. 不同的浏览器存放 cookie 的位置不同
  2. cookie 的存储是以域名形式进行区分的, 不同域下的 cookie 是独立的
  3. 可以设置 cookie 生效的域
  4. 一个域下存放的 cookie 数量是有限的, 不同的浏览器存放的个数不同.
  5. 每个 cookie 存放的内容大小不同, 一般为 4kb
  6. cookie 默认会话结束自动销毁, 可以设置过期时间

cookie可以分为两种不同类型: 会话cookie和持久cookie.

  • 如果cookie不包含到期日期, 就是会话cookie, 会话cookie存储的内存中, 不会写入磁盘, 浏览器关闭的时候, cookie就会被丢弃
  • 如果cookie包含日期, 就是持久性cookie, 会在指定的日期后从磁盘删除

Cookie 操作

设置 Cookie

document.cookie = '名字=值';
document.cookie = 'username=cfangxu;domain=baike.baidu.com'; //并且设置了生效域
复制代码

服务端设置 cookie,

服务端返回的 response header 中有意向叫 set-cookie, 是服务端专门用来设置 cookie 的,

Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
复制代码

一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。

服务端会通过请求头部的cookie字段读取cookie相关的信息.

读取 Cookie

我们可以通过document.cookie来获取当前网站下的 cookie 的时候, 就可以得到当前网站下的所有 cookie.

修改 Cookie

想要修改一个 Cookie, 只要重新复制就可以了, 旧的值会被新的值覆盖, 但是在设置新的 cookie 时, path/domain 这几个要保持一直, 否则就是添加一个新的 cookie 了.

删除 cookie

将 cookie 的过期时间设置为已经过去的时间, 同样的 path/domain 需要保持一致.

工具函数

创建 cookie

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
    var expires = 'expires=' + d.toGMTString();
    document.cookie = cname + '=' + cvalue + '; ' + expires;
}
复制代码

获取 cookie

function getCookie(cname) {
    var name = cname + '=';
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
    }
    return '';
}
复制代码

Cookie 选项

  • domain: 指定 cookie 将要被发送至哪个或哪些域中. 默认情况下, domain 会被设置为创建该 cookie 的页面所在的域名, 所以当给相同域名发送请求时该 cookie 会被发送给服务器.
  • path: 指定目录或子目录下的网页才可以访问, 即 path 属性
  • secure: 确保 cookie 在 https 或者其他安全协议中才能被发送至服务器
  • httpOnly: 设置 cookie 是否能通过 js 去访问. 默认 httpOnly 选项为空, 客户端是可以通过 js 代码访问的.
  • same-site: 用来防止CSRF攻击和用户追踪

same-site

SameSite属性用来限制第三方Cookie, 从而减少安全风险.

它可以设置三个值:

  1. strict: 严格, 完全静止第三方的cookie, 跨站点的时候, 任何情况下都不会发送cookie.
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
复制代码
  1. lax: 大部分情况下不发送第三方cookie, 但是导航到目标网址的get请求除外.

导航到目标网站的GET请求, 只包含三种情况: 链接, 预加载请求, GET请求:

请求类型 示例 正常情况 Lax
链接 <a href="https://juejin.cn/post/..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="https://juejin.cn/post/..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="https://juejin.cn/post/..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="https://juejin.cn/post/..."> 发送 Cookie 不发送
  1. None: lax会变为默认的设置, 这时, 网站可以选择显示关闭samesite, 将其设置为None. 不过前提是必须同时设置secure属性(Cookie只能通过HTTPS协议发送), 否则无效.

无效设置:

Set-Cookie: widget_session=abc123; SameSite=None
复制代码

有效设置:

Set-Cookie: widget_session=abc123; SameSite=None; Secure
复制代码

LocalStorage

HTML5 的新方法, 兼容 IE8 以上的浏览器

特点

  • 生命周期: 持久化的本地存储, 需要手动删除数据, 否则不会过期
  • 存储的信息在同一个域中是共享的
  • 当本页操作了 LocalStorage, 本页面不会触发 storage 事件, 但是别的页面会触发 storage 事件
  • 大小: 与浏览器有关, 一般为 5M, 值得说明的是,安卓上手 Q 、手机QQ浏览器、微信中则是 2.5M 的数量级,因此在移动端,本地存储的size更加珍贵
  • 在非 IE 浏览中可以本地打开, IE 需要在服务器中打开
  • LocalStorage 本事还是对字符串的读取, 如果存储内容多会消耗内存空间, 导致页面卡顿.
  • localStorage 收到同源策略的限制

操作

设置:

localStorage.setItem('username', 'cfangxu');
复制代码

获取:

localStorage.getItem('username');

//获取第一个键名
localStorage.key(0);
复制代码

删除:

localStorage.removeItem('username');

//一次清除所有存储
localStorage.clear();
复制代码

Storage 事件

当 storage 发生改变的时候触发。
注意: 当前页面对 storage 的操作会触发其他页面的 storage 事件
事件的回调函数中有一个参数 event,是一个 StorageEvent 对象,提供了一些实用的属性,如下表:

Property Type Description
key String The named key that was added, removed, or moddified
oldValue Any The previous value(now overwritten), or null if a new item was added
newValue Any The new value, or null if an item was added
url/uri String The page that called the method that triggered this change

模拟实现一个 localStorage

if (!window.localStorage) {
    window.localStorage = {
        getItem: function(sKey) {
            if (!sKey || !this.hasOwnProperty(sKey)) {
                return null;
            }
            return unescape(
                document.cookie.replace(
                    new RegExp(
                        '(?:^|.*;\\s*)' +
                            escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') +
                            '\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*'
                    ),
                    '$1'
                )
            );
        },
        key: function(nKeyId) {
            return unescape(
                document.cookie.replace(/\s*\=(?:.(?!;))*$/, '').split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]
            );
        },
        setItem: function(sKey, sValue) {
            if (!sKey) {
                return;
            }
            document.cookie = escape(sKey) + '=' + escape(sValue) + '; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/';
            this.length = document.cookie.match(/\=/g).length;
        },
        length: 0,
        removeItem: function(sKey) {
            if (!sKey || !this.hasOwnProperty(sKey)) {
                return;
            }
            document.cookie = escape(sKey) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
            this.length--;
        },
        hasOwnProperty: function(sKey) {
            return new RegExp('(?:^|;\\s*)' + escape(sKey).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=').test(
                document.cookie
            );
        }
    };
    window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length;
}
复制代码

上面是用 cookie 实现 LocalStorage, 然后是的 localStorage 支持 expires:

(function() {
    var getItem = localStorage.getItem.bind(localStorage);
    var setItem = localStorage.setItem.bind(localStorage);
    var removeItem = localStorage.removeItem.bind(localStorage);
    localStorage.getItem = function(keyName) {
        var expires = getItem(keyName + '_expires');
        if (expires && new Date() > new Date(Number(expires))) {
            removeItem(keyName);
            removeItem(keyName + '_expires');
        }
        return getItem(keyName);
    };
    localStorage.setItem = function(keyName, keyValue, expires) {
        if (typeof expires !== 'undefined') {
            var expiresDate = new Date(expires).valueOf();
            setItem(keyName + '_expires', expiresDate);
        }
        return setItem(keyName, keyValue);
    };
})();
复制代码

使用如下:

localStorage.setItem('key', 'value', new Date() + 10000); // 10 秒钟后过期
localStorage.getItem('key');
复制代码

SessionStorage

类似于 localstorage, 用于临时保存同一窗口(或标签页)的数据,与 localstorage 不同的是, sessionstorage 会在关闭窗口或标签页之后将会删除这些数据。

操作

设置:

sessionStorage.setItem('key', 'value');
复制代码

读取:

var lastname = sessionStorage.getItem('key');
复制代码

删除:

sessionStorage.removeItem('key');
复制代码

清除:

sessionStorage.clear();
复制代码

IndexedDb

IndexedDB 就是浏览器提供的本地数据库, 可以被网页脚本创建和操作, 允许存储大量数据, 提供查询接口, 简历索引. 不属于不关系型数据库, 更接近 NoSql

IndexedDb 特点

  1. 键值对存储: 每一条数据都有对应的主键, 主键独一无二
  2. 异步: 数据库操作不会锁死浏览器, 与 localstorage 不同, 后者是同步的.
  3. 支持事务:在一些列操作步骤之中, 只要有异步失败, 整个事务就都取消, 数据库回滚到事务发生之前的状态.
  4. 同原限制: 每个数据库对应创建它的域名, 网页只能访问自身域名下的数据库, 而不能访问跨域的数据库
  5. 存储空间大: 比 LocalStorage 大的多, 一般不少于 250MB, 甚至没有上限
  6. 支持二进制存储: 不仅可以存储字符串, 还可以存储二进制数据(ArrayBuffer 对象和 Blob 对象)

IndexedDB 概念

  • 数据库: 数据库是一系列相关数据的容器, 每个域名(协议+域名+端口)都可以新建任意多个数据库, IndexedDB 数据库有版本的概念, 同一个时刻只能有一个版本的数据库存在, 如果要修改数据库结构, 只能通过升级数据库版本完成.
  • 对象仓库: 每个数据库包含若干个对象仓库(object store), 它类似于关系型数据库的表格
  • 数据记录: 对象仓库保存的是数据记录, 每条记录类似于关系型数据库的行, 但是只有主键和数据体来两部分, 主键用来建立默认的索引, 必须是不同的, 否则会报错. 主键可以使数据记录里面的一个属性, 也可以指定为一个递增的整数编号.
  • 索引: 为了加速数据的检索, 可以再对象仓库里面, 为不同的属性建立索引
  • 事务: 数据记录的读写和删改, 都要通过事务完成, 事务对象提供error,aboutcomplate三个事件, 用来监听操作结果.

操作

打开数据库

var request = window.indexedDB.open(databaseName, version);
复制代码
  • dataBaseName: 字符串, 数据库
  • version: 整数, 默认为 1

返回一个 IDBRequest 对象, 通过三种事件处理结果:

  1. error
request.onerror = function(event) {
    console.log('数据库打开报错');
};
复制代码
  1. success
var db;

request.onsuccess = function(event) {
    db = request.result;
    console.log('数据库打开成功');
};
复制代码
  1. upgradeneeded

如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件 upgradeneeded。

var db;

request.onupgradeneeded = function(event) {
    db = event.target.result;
};
复制代码

新建数据库

新建数据库与打开数据库是同一个操作, 如果指定的数据库不存在, 就会新建. 不同之处在于后续的操作主要在upgradeneeded事件的监听函数中完成.

通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。

request.onupgradeneeded = function(event) {
    db = event.target.result;
    var objectStore = db.createObjectStore('person', { keyPath: 'id' });
};
复制代码

当然最好的创建之前判断一下, 不存在再新建:

request.onupgradeneeded = function(event) {
    db = event.target.result;
    var objectStore;
    if (!db.objectStoreNames.contains('person')) {
        objectStore = db.createObjectStore('person', { keyPath: 'id' });
    }
};
复制代码

主键key是默认建立索引的属性. 也可以让 IndexedDB 自动生成主键:

var objectStore = db.createObjectStore('person', { autoIncrement: true });
复制代码

建立对象以后, 下一步可以新建索引:

request.onupgradeneeded = function(event) {
    db = event.target.result;
    var objectStore = db.createObjectStore('person', { keyPath: 'id' });
    objectStore.createIndex('name', 'name', { unique: false });
    objectStore.createIndex('email', 'email', { unique: true });
};
复制代码

三个参数分别是索引名称, 索引所在的属性, 配置对象(说明该属性是否包含重复的值)

新增数据

新增数据指向对象仓库写入数据, 这需要通过事务完成:

function add() {
    var request = db
        .transaction(['person'], 'readwrite')
        .objectStore('person')
        .add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });

    request.onsuccess = function(event) {
        console.log('数据写入成功');
    };

    request.onerror = function(event) {
        console.log('数据写入失败');
    };
}

add();
复制代码

写入数据需要新建一个事务, 新建是必须制定表格名称和操作模式. 新建事务以后, 通过IDBTransaction.objectStore(name)方法, 拿到IDBObjectStore对象, 在通过表格对象add()想表哥写入一条记录.

读取数据

读取数据也只能通过事务完成:

function read() {
    var transaction = db.transaction(['person']);
    var objectStore = transaction.objectStore('person');
    var request = objectStore.get(1);

    request.onerror = function(event) {
        console.log('事务失败');
    };

    request.onsuccess = function(event) {
        if (request.result) {
            console.log('Name: ' + request.result.name);
            console.log('Age: ' + request.result.age);
            console.log('Email: ' + request.result.email);
        } else {
            console.log('未获得数据记录');
        }
    };
}

read();
复制代码

objectStore.get()用于读取数据, 参数是主键的值

遍历数据

遍历数据表格的所有记录,要使用指针对象 IDBCursor

function readAll() {
    var objectStore = db.transaction('person').objectStore('person');

    objectStore.openCursor().onsuccess = function(event) {
        var cursor = event.target.result;

        if (cursor) {
            console.log('Id: ' + cursor.key);
            console.log('Name: ' + cursor.value.name);
            console.log('Age: ' + cursor.value.age);
            console.log('Email: ' + cursor.value.email);
            cursor.continue();
        } else {
            console.log('没有更多数据了!');
        }
    };
}

readAll();
复制代码

上面代码中,新建指针对象的openCursor()方法是一个异步操作,所以要监听 success 事件。

更新数据

function update() {
    var request = db
        .transaction(['person'], 'readwrite')
        .objectStore('person')
        .put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });

    request.onsuccess = function(event) {
        console.log('数据更新成功');
    };

    request.onerror = function(event) {
        console.log('数据更新失败');
    };
}

update();
复制代码

删除数据

function remove() {
    var request = db
        .transaction(['person'], 'readwrite')
        .objectStore('person')
        .delete(1);

    request.onsuccess = function(event) {
        console.log('数据删除成功');
    };
}

remove();
复制代码

使用索引

索引的意义在于, 可以让你搜索任意字段, 也就是说从任意字段拿到数据记录. 如果不建立索引, 默认只能搜索主键:

//建立索引
objectStore.createIndex('name', 'name', { unique: false });

//通过索引数据
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');

request.onsuccess = function(e) {
    var result = e.target.result;
    if (result) {
        // ...
    } else {
        // ...
    }
};
复制代码

其他: WebSQL

2010 年被 W3C 废弃的本地数据库数据存储方案,但是主流浏览器(火狐除外)都已经有了相关的实现,web sql 类似于 SQLite,是真正意义上的关系型数据库,用 sql 进行操作,当我们用 JavaScript 时要进行转换,较为繁琐。

参考链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享