如何选择 Web 的数据存储方式?看我就够了

377 阅读9分钟

1. 前言

为了最大限度地保证同一浏览器同一域名下各个网页的用户统一,Web JS SDK 需要及时地将用户标识存入到 Cookie;

为了最大限度地减少关闭页面导致的数据丢失,Web JS SDK 将采集的数据存入到 localStorage 里进行批量发送,关闭页面未发送完的数据下次打开页面再次发送;

为了最大限度地保证可视化全埋点和网页热力图窗口打开的正确性,Web JS SDK 将相关的标识存入到 sessionStorage 里。

由此可见,存储数据是 Web JS SDK 的核心功能,下面逐一给大家介绍这三种存储方式。

2. 存储方式 

2.1. Cookie

Cookie 实际上是一小段的文本信息(key-value 格式)。客户端向服务端发起请求,如果服务端需要记录该用户的状态,就使用 response 向客户端浏览器颁发一个 Cookie。如图 2-1 所示:

图 2-1 服务端使用 response 向客户端浏览器颁发一个 Cookie.png 图 2-1 服务端使用 response 向客户端浏览器颁发一个 Cookie

客户端浏览器会把 Cookie 保存起来,当浏览器再次请求该网站时,浏览器把请求的网址连同 Cookie 一起提交给服务端。服务端检查该 Cookie,以此来辨认用户状态。如图 2-2 所示:

图 2-2 浏览器把 Cookie 提交给服务端.png 图 2-2 浏览器把 Cookie 提交给服务端

Web JS SDK 中使用 Cookie 功能主要是用来存储前端变量。每当同一台设备通过浏览器请求集成 Web JS SDK 的页面时,就会读取已经保存的 Cookie 值。

2.2. localStorage

localStorage 用于持久化的本地存储,没有过期时间。除非主动删除数据,否则数据是永远不会过期的。

localStorage 提供的存储也是基于字符串的键值对。可以通过 setItem()、getItem() 来访问其中的存储项。

2.3. sessionStorage

用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问,并且当会话结束后数据也随之销毁。

因此,sessionStorage 不是一种持久化的本地存储,仅仅是会话级别的存储。浏览器网页关闭时,对应的 sessionStorage 就会被清空。

2.4. 三者的区别

三种存储方式的区别如表 2-1 所示:

 CookielocalStoragesessionStorage
应用场景浏览器和服务端来回传递。不会自动把数据发送给服务端,用于本地持久保存。不会自动把数据发送给服务端,用于本地同一会话保存。
存储大小数据大小不能超过 4K。因为每次 http 请求都会携带 Cookie,所以 Cookie 只适合保存很小的数据。数据大小可以在 5M 以上。保存的数据超出了浏览器所规定的大小,不会清空旧数据只会报错。但是,在移动端浏览器和 App 的 WebView 里是不可靠的,可能会因为内存不足或网络切换被清空。数据大小可以在 5M 以上。保存的数据超出了浏览器所规定的大小,不会清空旧数据只会报错,但在移动端浏览器和 app 的 Webview 里是不可靠的,可能会因为内存不足或网络切换被清空。
数据有效期只在设置的过期时间之前有效,即使窗口或浏览器关闭。不管浏览器关闭与否,数据永久保存。只在当前浏览器关闭之前有效。
作用域数据在所有同源窗口中都是共享的。同源窗口共享(相同协议、域名、端口号)。在不同浏览器窗口不共享,即使是同一个页面。
兼容性兼容性最强。低版本浏览器不支持。低版本浏览器不支持。

表 2-1 三种存储方式的区别

3. Web JS SDK 中不同存储方式的应用场景

3.1. Cookie 的应用场景

SDK 初始化时从 Cookie 中读取用户标识,没有用户标识就生成新的 UUID,将新的 UUID 存储到浏览器 Cookie 中。

首次加载页面,Cookie 中无用户信息,将新生成的 UUID 保存到 Cookie 中。代码如下:

| var uuid = _.UUID();``   ``var cross = _.cookie.get(this.getCookieName());``   ``cross = _.cookie.resolveValue(cross);``   ``if (cross === null) {``     ``// null 肯定是首次,非 null,看是否有 distinct_id``     ``sd.is_first_visitor = ``true``;      ``this.``set``(``'distinct_id'``, uuid);``   ``} ``else {``     ``if (!_.isJSONString(cross) || !JSON.parse(cross).distinct_id) {``       ``sd.is_first_visitor = ``true``;``     ``}      ``this.toState(cross);``   ``} |

首次加载集成 Web JS SDK 的页面,浏览器中 Cookie 的用户信息如图 3-1 所示:

图 3-1 浏览器中 Cookie 的用户信息.png

图 3-1 浏览器中 Cookie 的用户信息

调用 sensors.login() 实现用户关联,将登录 ID 存储到 Cookie 中。distinct_id 为登录 ID,first_id 为匿名 ID。代码如下:

sd.login = ``function (``id``, callback) {``  ``if (typeof ``id === ``'number'``) {``    ``id = String(``id``);``  ``}``  ``if (saEvent.check({ distinct_id: ``id })) {``    ``var firstId = store.getFirstId();``    ``var distinctId = store.getDistinctId();``    ``if (``id !== distinctId) {``      ``if (!firstId) {``        ``store.``set``(``'first_id'``, distinctId);``      ``}``      ``sd.trackSignup(``id``, ``'$SignUp'``, {}, callback);``    ``} ``else {``      ``callback && callback();``    ``}``  ``} ``else {``    ``sd.log(``'login 的参数必须是字符串'``);``    ``callback && callback();``  ``}``};

浏览器中 Cookie 的用户信息将更新成如图 3-2 所示:

图 3-2 浏览器中 Cookie 的用户信息更新.png

图 3-2 浏览器中 Cookie 的用户信息更新

调用 sensors.logout() 实现用户退出登录,将 Cookie 中的 distinct_id 修改为匿名 ID,first_id 置空。代码如下:

sd.``logout = ``function (isChangeId) {``  ``var firstId = store.getFirstId();``  ``if (firstId) {``    ``store.``set``(``'first_id'``, ``''``);``    ``if (isChangeId === ``true``) {``      ``var uuid = _.UUID();``      ``store.``set``(``'distinct_id'``, uuid);``    ``} ``else {``      ``store.``set``(``'distinct_id'``, firstId);``    ``}``  ``} ``else {``    ``sd.log(``'没有 first_id,logout 失败'``);``  ``}``};

浏览器中 Cookie 的用户信息将更新成如上图 3-1 所示。

调用 sensors.identify("niming",true) 修改匿名 ID。代码如下:

sd.identify = ``function (``id``, isSave) {``   ``if (typeof ``id === ``'number'``) {``     ``id = String(``id``);``   ``}``   ``var firstId = store.getFirstId();``   ``if (typeof ``id === ``'undefined'``) {``     ``var uuid = _.UUID();``     ``if (firstId) {``       ``store.``set``(``'first_id'``, uuid);``     ``} ``else {``       ``store.``set``(``'distinct_id'``, uuid);``     ``}``   ``} ``else if (saEvent.check({ distinct_id: ``id })) {``     ``if (isSave === ``true``) {``       ``if (firstId) {``         ``store.``set``(``'first_id'``, ``id``);``       ``} ``else {``         ``store.``set``(``'distinct_id'``, ``id``);``       ``}``     ``} ``else {``       ``if (firstId) {``         ``store.change(``'first_id'``, ``id``);``       ``} ``else {``         ``store.change(``'distinct_id'``, ``id``);``       ``}``     ``}``   ``} ``else {``     ``sd.log(``'identify 的参数必须是字符串'``);``   ``}`` ``};

登录状态下修改匿名 ID 后 Cookie 的更新如图 3-3 所示:

图 3-3 登录状态下修改匿名 ID 后的 Cookie.png

图 3-3 登录状态下修改匿名 ID 后的 Cookie

匿名状态下修改匿名 ID 后 Cookie 的更新如图 3-4 所示:

图 3-4 匿名状态下修改匿名 ID 后的 Cookie.png

图 3-4 匿名状态下修改匿名 ID 后的 Cookie

调用 sensors.register() 注册的公共属性会写入 Cookie,在同域名的子页面之间共享。另外,Web JS SDK 渠道相关的属性也写入 Cookie。

Web JS SDK 通过在 Cookie 中保存变量来实现同域名下各个页面的变量共享。

3.1.1. 存储、获取和删除

 Cookie 的存储、获取和删除不像 localStorage 有自己的 API 可以直接调用,Cookie 的这些操作都是基于 document.cookie 进行封装的。代码如下:

document.cookie = valid_name + ``'=' + encodeURIComponent(valid_value) + expires + ``'; path=/' + valid_domain + samesite + secure;``get: ``function (name) {``      ``var nameEQ = name + ``'='``;``      ``var ca = document.cookie.``split``(``';'``);``      ``for (var i = 0; i < ca.length; i++) {``        ``var c = ca[i];``        ``while (c.charAt(0) == ``' '``) {``          ``c = c.substring(1, c.length);``        ``}``        ``if (c.indexOf(nameEQ) == 0) {``          ``return _.decodeURIComponent(c.substring(nameEQ.length, c.length));``        ``}``      ``}``      ``return null;``},``remove: ``function (name, cross_subdomain) {``      ``cross_subdomain = typeof cross_subdomain === ``'undefined' ? sd.para.cross_subdomain : cross_subdomain;``      ``_.cookie.``set``(name, ``''``, -1, cross_subdomain);``},

3.1.2. 属性项

如果没有在浏览器中设置过期时间,Cookie 将会保存在内存中,生命周期随着浏览器的关闭而结束,这种被称为会话 Cookie;如果设置了过期时间,浏览器就会把 Cookie 保存在硬盘上,关闭后再次打开浏览器这些 Cookie 依然有效,直到过期时间 Cookie 才会被清除。

详细的属性说明如表 3-1 所示:

属性项说明
NAME=VALUE 键值对Cookie 名称和 Cookie 值必须设置,这里的 NAME 不能和其他属性项的名字一样。
Expires过期时间,在设置的某个时间点后该 Cookie 就会失效。
max-age告诉浏览器多长时间过期,单位是秒。不是一个固定的时间点,max-age 的优先级高于 Expires。
Domain生成该 Cookie 的域名,如 domain="www.sensorsdata.cn",根域或者子域。
Path该 Cookie 是在当前的哪个路径下生成的,一般设置为 "/",同一站点下所有页面都可以访问这个 Cookie。
HttpOnly告知浏览器不允许通过脚本 document.cookie 去更改这个值,同样这个值在 document.cookie 中也不可见。但是,在 http 请求中仍然会携带这个 Cookie。注意:这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在,这项设置通常在服务端设置。
SameSite防止跨站请求伪造(CSRF)攻击和保护用户隐私。
Secure如果设置了这个属性,那么只有通过 https 协议连接时 Cookie 才可以被页面访问。

表 3-1 Cookie 的属性说明

3.1.3. 安全性

Cookie 存储在客户端浏览器里,在控制台下运行 document.cookie 就可以查看。因此,Cookie 存储的信息很容易被窃取。

基于安全的考虑,需要给 Cookie 加上 Secure 和 HttpOnly,属性设置 HttpOnly=true,JS 无法用 document.cookie 获取 Cookie 的内容,设置 Secure=true,Cookie 只能用 https 协议发送给服务端,用 http 协议是不发送的。

把 Cookie 的 Secure 设置为 true,只保证 Cookie 与服务端之间的数据传输过程加密,而保存在本地的 Cookie 文件并不加密。如果想让本地 Cookie 也加密,需要自己加密数据。Web JS SDK 给浏览器中 Cookie 的 value 值设置了加密。代码如下:

encrypt: ``function (``v``) {``     ``return 'data:enc;' + _.rot13obfs(``v``);``   ``},``decrypt: ``function (``v``) {``     ``v = ``v``.substring(``'data:enc;'``.length);``     ``v = _.rot13defs(``v``);``     ``return v``;``   ``},

3.1.4. 优缺点

优点:

  1. 同一网站中所有页面共享一套 Cookie,可以实现同域名下跨页面的全局变量共享。例如:Web JS SDK 中的用户变量、公共属性变量、渠道参数变量;
  2. 兼容性强;
  3. 可灵活设置数据有效期。

缺点:

  1. 可能会被禁用。而 Cookie 被禁用会导致用户不能统一,采集的数据不准确;
  2. 安全性不高。打开控制台就可以查看 Cookie 信息,暴露用户隐私;
  3. 存储空间小。存储数据大小不能超过 4K;
  4. 可以被用户手动清空。用户手动清空 Cookie 会导致:同一用户同一浏览器下访问相同的网页被识别成不同的用户。

3.2. localStorage 的应用场景

由于浏览器的特性,关闭页面前发送请求有可能会失败。Web JS SDK 批量发送模式会把采集的数据存储到 localStorage 里,满足条件后合并发送,数据发送请求成功后才会删除 localStorage 里的数据。由于关闭页面、网络状态等原因导致数据发送请求失败,不会删除 localStorage 里的数据。当刷新页面或进入同域名的新页面时,如果网络条件恢复正常,会继续发送缓存的数据。这样以来,可以大大地减少数据丢失概率。代码如下:

writeStore: ``function (data) {``    ``var uuid = String(_.getRandom()).slice(2, 5) + String(_.getRandom()).slice(2, 5) + String(new Date().getTime()).slice(3);``    ``localStorage.setItem(``'sawebjssdk-' + uuid, JSON.stringify(data));`` ``} readStore: ``function () {``    ``var keys = [];``    ``var vals = [];``    ``var obj = {};``    ``var val = null;``    ``var now = new Date().getTime();``    ``var len = localStorage.length;``    ``var pendingItems = this.getPendingItems();``    ``for (var i = 0; i < len; i++) {``      ``var itemName = localStorage.key(i);``      ``if (itemName.indexOf(``'sawebjssdk-'``) === 0 && /^sawebjssdk-\d+$/.``test``(itemName)) {``        ``if (pendingItems.length && _.indexOf(pendingItems, itemName) > -1) {``          ``continue``;``        ``}``        ``val = localStorage.getItem(itemName);``        ``if (val) {``          ``val = _.safeJSONParse(val);``          ``if (val && _.isObject(val)) {``            ``val._flush_time = now;``            ``keys.push(itemName);``            ``vals.push(val);``          ``} ``else {``            ``localStorage.removeItem(itemName);``            ``sd.log(``'localStorage-数据parse异常' + val);``          ``}``        ``} ``else {``          ``localStorage.removeItem(itemName);``          ``sd.log(``'localStorage-数据取值异常' + val);``        ``}``      ``}``    ``}``    ``return {``      ``keys: keys,``      ``vals: vals``    ``};``  ``},

触发事件就写入 localStorage,控制台可查看 localStorage 存储的数据, 如图 3-5 所示:

图 3-5 控制台查看 localStorage 存储的数据.png

图 3-5 控制台查看 localStorage 存储的数据

注意:基本上可以认为绝大多数主流浏览器都已经支持了 localStorage,而 IE 是个特例。虽然 IE8 支持了 localStorage,但是更低版本的 IE 都不支持,对于不支持 localStorage 的浏览器默认实时发送。

localStorage 也可以像 Cookie 一样存储变量,Web JS SDK 的网页热力图分析中就使用 localStorage 来存储 jsonp 标识,页面加载时根据 localStorage 里有无这个标识判断用 ajax 方式还是 jsonp 方式请求数据。

3.2.1. API

3.2.1.1. 查看

类似 Chrome、Safari 这样的浏览器可以直接打开开发者工具,在 application 下的 localStorage 里直接查看数据。而 IE 需要在控制台中输入 localStorage 来查看数据,如图 3-6 所示:

图 3-6 IE 在控制台中输入 localStorage 来查看数据.png

图 3-6 IE 在控制台中输入 localStorage 来查看数据

3.2.1.2. 存储

使用 localStorage 的时候,我们需要判断浏览器是否支持 localStorage 这个属性,调用 localStorage.setItem() 来存储数据。代码如下:

if``(! window.localStorage){``    ``alert(``"浏览器不支持localstorage"``);``    ``return false``;``}``else``{``    ``localStorage.setItem(``'sensors_heatmap_method'``, ``'jsonp'``)``}

3.2.1.3. 获取

localStorage 根据 Item 获取采集数据的值,例如:JSON.parse(localStorage.getItem("sawebjssdk-3465518851230600")),获取的值为 String 类型,需要自己转换成对象。如图 3-7 所示:

图 3-7 localStorage 根据 Item 获取采集数据的值.png

图 3-7 localStorage 根据 Item 获取采集数据的值

3.2.1.4. 删除

清除 localStorage 某条信息,例如:localStorage.removeItem("sawebjssdk-3465518851230600"),表示清除 sawebjssdk-3465518851230600 这一条数据。

清除 localStorage 保存对象的全部数据:localStorage.clear()。如图 3-8 所示:

图 3-8 清除 localStorage 保存对象的全部数据.png

图 3-8 清除 localStorage 保存对象的全部数据

3.2.2. 优缺点

优点:

  1. 能够长期大量地保存浏览器的数据。Web JS SDK 触发的事件可以保存到 localStorage 里,除非手动删除否则不会丢失;
  2. 存储的数据不会主动发送到服务端;
  3. 更多丰富易用的接口。

缺点:

  1. 协议名、主机名和端口名都必须相同才能共享 localStorage。这点与 Cookie 不同:相同的域名不同的协议 Cookie 会共享,但 localStorage 不共享;
  2. 保存的数据超出了浏览器的大小,不会主动删除数据但控制台会报错;
  3. 兼容性不强,IE8 以下版本不支持,在移动设备上的浏览器或 Native App 用到的 WebView 里,localStorage 都是不可靠的。可能会因为各种原因(例如:退出 App、网络切换、内存不足等)被清空,Android 里用到的 WebView 必须手动开启 localStorage,否则是不支持的;
  4. 浏览器隐私模式下不支持 localStorage 的读取;
  5. 安全性低,容易被恶意代码注入。

3.3. sessionStorage 的应用场景

Web JS SDK 可视化全埋点就是通过使用 sessionStorage 来存储和读取 sensors-visual-mode 标识,判断网页是可视化全埋点模式还是采集模式。代码如下:

设置标识: sessionStorage.setItem(``'sensors-visual-mode'``, ``'true'``)``获取标识: sessionStorage.getItem(``'sensors-visual-mode'``)``删除标识: sessionStorage.removeItem(``'sensors-visual-mode'``)

Web JS SDK 网页热力图也是通过用 sessionStorage 来存储和读取 sensors_heatmap_url 和 sensors_heatmap_id 两个标识来判断网页是网页热力图模式还是采集模式。浏览器中查看 sessionStorage 的方式如图 3-9 所示:

图 3-9 浏览器中查看 sessionStorage 的方式.png 图 3-9 浏览器中查看 sessionStorage 的方式

网页热力图和可视化全埋点都是通过指定位置打开网页,将对应的标识存入到 sessionStorage 里。如果不是通过指定位置打开,相同的地址新打开一个窗口为新的会话。因为 sessionStorage 在不同会话中是不共享的,所以相同的地址打开的页面为采集模式。

3.3.1. API

sessionStorage 中的 API 与上文中的 LocalStorage 用法一样,这里不再赘述。

3.3.2. 优缺点

优点:

  1. 能够大量地保存浏览器中的数据;
  2. 在不同的窗口之下的 sessionStorage 存储相互独立、互不干扰。

缺点:

  1. 兼容性不强,IE8 以下版本不支持;
  2. 安全性低。

4. 总结

相信通过本文,我们对浏览器数据存储有了一个大致的了解。没有一种浏览器存储数据方式是完美的,都有一定的优缺点。因此,面对不同的需求时,我们要选择不同的方式来存储前端数据:

  • 如果存储的数据量较小,用于服务端请求,应用的网页范围较广,可以使用 Cookie 存储;
  • 如果存储的数据量较大,数据不用于进行服务端请求,并需要永久保存,可以使用 localStorage 存储;
  • 如果存储的数据量较大,关闭窗口后数据就清空,相同域名不同会话之间数据不共享,可以使用 sessionStorage 存储。

需要注意的是:不是什么数据都适合存储到 Cookie、localStorage 和 sessionStorage 中的。因为它们保存在本地容易被篡改,使用它们的时候需要注意是否有恶意代码注入的风险,所以千万不要用它们存储敏感数据。

文章来源公众号——神策技术社区