跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

本文概述

  • API可能是无状态的, 人机交互不是
  • 四千个Cookie
  • 片段标识符
  • 网络存储
  • 索引数据库
  • 总结
假设我正在访问一个网站。我右键单击其中一个导航链接, 然后选择在新窗口中打开该链接。应该怎么办?如果我希望与大多数用户一样, 我希望新页面的内容与直接单击链接一样。唯一的区别应该是该页面将出现在新窗口中。但是, 如果你的网站是单页应用程序(SPA), 则可能会看到奇怪的结果, 除非你对此情况进行了精心计划。
回想一下, 在SPA中, 典型的导航链接通常是片段标识符, 以井号(#)开头。直接单击链接不会重新加载页面, 因此将保留JavaScript变量中存储的所有数据。但是, 如果我在新标签页或窗口中打开链接, 浏览器会重新加载页面, 并重新初始化所有JavaScript变量。因此, 除非你已采取措施以某种方式保留该数据, 否则绑定到这些变量的所有HTML元素都将显示不同。
跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

文章图片
跨页面重新加载的持久数据:Cookie, IndexedDB及其之间的所有内容
鸣叫
如果我明确地重新加载页面(例如, 按F5键), 也会出现类似的问题。你可能认为我永远不需要按F5, 因为你已经建立了一种机制, 可以自动从服务器推送更改。但是, 如果我是普通用户, 可以打赌我仍然会重新加载页面。也许我的浏览器似乎没有正确地重新粉刷屏幕, 或者我只是想确定自己拥有最新的股票报价。
API可能是无状态的, 人机交互不是 与通过RESTful API发出的内部请求不同, 人类用户与网站的互动并非没有状态。作为网络用户, 我认为我对你网站的访问是一个会话, 几乎就像一个电话。我希望浏览器能够记住与我的会话有关的数据, 就像我打电话给你的销售或支持热线时一样, 我希望代表能够记住呼叫中先前所说的内容。
跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

文章图片
会话数据的一个明显例子是我是否登录以及(如果已登录)哪个用户。进入登录屏幕后, 我应该能够自由浏览网站的用户特定页面。如果我在新标签页或窗口中打开链接, 并看到另一个登录屏幕, 则说明该界面不是很友好。
另一个示例是电子商务站点中购物车的内容。如果按F5键清空购物车, 则用户可能会感到不高兴。
在用PHP编写的传统多页应用程序中, 会话数据将存储在$ _SESSION超全局数组中。但是在SPA中, 它必须位于客户端的某个位置。在SPA中存储会话数据有四个主要选项:
  • 曲奇饼
  • 片段标识符
  • 网络存储
  • 索引数据库
四千个Cookie Cookies是浏览器中Web存储的一种较旧形式。它们最初旨在将从服务器接收的数据存储在一个请求中, 然后在后续请求中将其发送回服务器。但是通过JavaScript, 你可以使用cookie来存储几乎任何类型的数据, 每个cookie的最大大小限制为4 KB。 AngularJS提供了用于管理cookie的ngCookies模块。还有一个js-cookies包, 在任何框架中都提供类似的功能。
跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

文章图片
请记住, 无论页面重新加载还是Ajax请求, 你创建的任何cookie都会在每次请求时都发送到服务器。但是, 如果你需要存储的主要会话数据是已登录用户的访问令牌, 则无论如何, 你都希望将此数据发送到服务器。尝试使用这种自动cookie传输作为为Ajax请求指定访问令牌的标准方法是很自然的。
你可能会争辩说, 以这种方式使用cookie与RESTful体系结构不兼容。但是在这种情况下, 这很好, 因为通过API的每个请求仍然是无状态的, 具有一些输入和一些输出。只是其中一种输入是通过Cookie以有趣的方式发送的。如果你可以安排登录API请求也将访问令牌发送回cookie中, 那么你的客户端代码根本不需要处理cookie。同样, 它只是请求以不寻常的方式返回的另一个输出。
与网络存储相比, Cookie提供了一项优势。你可以在登录表单上提供” 保持登录状态” 复选框。使用语义, 我希望如果不检查它, 那么如果我重新加载页面或在新标签页或窗口中打开链接, 我将保持登录状态, 但是当关闭浏览器后, 我保证将注销。如果我使用共享计算机, 这是一项重要的安全功能。稍后我们将看到, 网络存储不支持此行为。
那么这种方法在实践中如何运作?假设你正在服务器端使用LoopBack。你已经定义了人员模型, 扩展了内置的用户模型, 并添加了要为每个用户维护的属性。你已将人员模型配置为通过REST公开。现在, 你需要调整server / server.js以获得所需的cookie行为。下面是server / server.js, 从slc loopback生成的内容开始, 带有已标记的更改:
var loopback = require('loopback'); var boot = require('loopback-boot'); var app = module.exports = loopback(); app.start = function() { // start the web server return app.listen(function() { app.emit('started'); var baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { var explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); }; // start of first change app.use(loopback.cookieParser('secret')); // end of first change// Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. boot(app, __dirname, function(err) { if (err) throw err; // start of second change app.remotes().after('Person.login', function (ctx, next) { if (ctx.result.id) { var opts = {signed: true}; if (ctx.req.body.rememberme !== false) { opts.maxAge = 1209600000; } ctx.res.cookie('authorization', ctx.result.id, opts); } next(); }); app.remotes().after('Person.logout', function (ctx, next) { ctx.res.cookie('authorization', ''); next(); }); // end of second change// start the server if `$ node server.js` if (require.main === module) app.start(); });

第一个更改将cookie解析器配置为使用” 秘密” 作为cookie签名秘密, 从而启用签名的cookie。你需要这样做, 因为尽管LoopBack会在Cookie的” 授权” 或” access_token” 中查找访问令牌, 但它要求对此类Cookie进行签名。实际上, 此要求毫无意义。签署Cookie旨在确保未修改Cookie。但是你不必修改访问令牌。毕竟, 你可能已经以普通的参数以无符号形式发送了访问令牌。因此, 除非你将签名的Cookie用于其他用途, 否则你不必担心很难猜测到Cookie的签名秘密。
第二个更改为Person.login和Person.logout方法设置了一些后处??理。对于Person.login, 你还想获取结果访问令牌并将其作为签名的cookie” 授权” 发送给客户端。客户端可以在凭据参数” rememberme” 中添加另一个属性, 以指示是否使cookie持续2周。默认值为true。 login方法本身将忽略此属性, 但后处理器将对其进行检查。
对于Person.logout, 你想清除此cookie。
你可以立即在StrongLoop API资源管理器中查看这些更改的结果。通常, 在Person.login请求之后, 你将必须复制访问令牌, 将其粘贴到右上角的表单中, 然后单击” 设置访问令牌” 。但是有了这些更改, 你无需执行任何操作。访问令牌会自动保存为Cookie” 授权” , 并在以后的每个请求中发送回去。当资源管理器显示来自Person.login的响应头时, 它会忽略cookie, 因为从不允许JavaScript看到Set-Cookie头。但是请放心, cookie在那里。
在客户端, 在重新加载页面时, 你会看到cookie” 授权” 是否存在。如果是这样, 则需要更新当前userId的记录。可能最简单的方法是在成功登录后将userId存储在单独的cookie中, 因此你可以在页面重新加载时检索它。
片段标识符 当我访问已实现为SPA的网站时, 浏览器地址栏中的URL可能类似于” https://example.com/#/my-photos/37″ 。其片段标识符部分” #/ my-photos / 37″ 已经是状态信息的集合, 可以将其视为会话数据。在这种情况下, 我可能正在查看我的一张照片, 即ID为37的照片。
跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

文章图片
你可以决定将其他会话数据嵌入片段标识符中。回想一下, 在上一节中, 访问令牌存储在cookie” 授权” 中, 你仍然需要以某种方式跟踪userId。一种选择是将其存储在单独的cookie中。但是另一种方法是将其嵌入片段标识符中。你可以决定当我登录时, 我访问的所有页面都有一个以” #/ u / XXX” 开头的片段标识符, 其中XXX是userId。因此, 在上一个示例中, 如果我的userId为59, 则片段标识符可能是” #/ u / 59 / my-photos / 37″ 。
从理论上讲, 你可以将访问令牌本身嵌入片段标识符中, 从而无需使用Cookie或网络存储。但这将是一个坏主意。然后, 我的访问令牌将在地址栏中可见。任何人用相机看着我的肩膀都可以拍摄屏幕快照, 从而可以访问我的帐户。
最后一点:可以设置SPA, 使其完全不使用片段标识符。而是使用普通的网址(例如” http://example.com/app/dashboard” 和” http://example.com/app/my-photos/37″ ), 并将服务器配置为返回你的顶级HTML SPA, 以响应对任何这些URL的请求。然后, 你的SPA根据路径(例如” / app / dashboard” 或” / app / my-photos / 37″ )而不是片段标识符进行路由。它拦截对导航链接的单击, 并使用History.pushState()推送新URL, 然后照常进行路由。它还侦听popstate事件, 以检测用户单击” 后退” 按钮, 然后再次在已还原的URL上进行路由。如何实现此功能的完整细节超出了本文的范围。但是, 如果使用此技术, 则显然可以将会话数据而不是片段标识符存储在路径中。
网络存储 Web存储是JavaScript在浏览器中存储数据的一种机制。像cookie一样, 网络存储对于每个来源都是独立的。每个存储的项目都有一个名称和一个值, 它们都是字符串。但是Web存储对于服务器是完全不可见的, 并且它提供的存储容量比cookie大得多。 Web存储有两种类型:本地存储和会话存储。
跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)

文章图片
在所有窗口的所有选项卡上都可以看到一个本地存储项, 即使关闭浏览器后该项仍将保留。在这方面, 它的行为有点像具有过期日期的cookie。因此, 在用户在登录表单上选中” 保持登录状态” 的情况下, 适合存储访问令牌。
会话存储项仅在创建该选项卡的选项卡中可见, 并且在该选项卡关闭时消失。这使得它的生命周期与任何cookie的生命周期都非常不同。回想一下, 会话cookie在所有窗口的所有选项卡上仍然可见。
如果你将AngularJS SDK用于LoopBack, 则客户端将自动使用网络存储来保存访问令牌和userId。这发生在js / services / lb-services.js中的LoopBackAuth服务中。它将使用本地存储, 除非RememberMe参数为false(通常意味着未选中” 保持登录状态” 复选框), 在这种情况下, 它将使用会话存储。
结果是, 如果我在未选中” 保持登录状态” 的情况下登录, 然后在新标签页或窗口中打开链接, 则不会在此处登录。我很可能会看到登录屏幕。你可以自己决定这是否可以接受。有人可能会认为它是一个不错的功能, 你可以在其中具有多个选项卡, 每个选项卡均以不同的用户身份登录。或者, 你可能决定几乎没有人再使用共享计算机, 因此你可以完全省略” 保持登录状态” 复选框。
那么, 如果你决定使用AngularJS SDK for LoopBack, 会话数据处理将如何?假设你在服务器端的情况与以前相同:定义了Person模型, 扩展了User模型, 并通过REST公开了Person模型。你将不会使用Cookie, 因此无需进行任何上述更改。
在客户端, 最外面的控制器中的某处, 你可能有一个类似$ scope.currentUserId的变量, 该变量保存当前登录用户的userId;如果该用户未登录, 则为null。然后, 要正确处理页面重新加载, 你可以只需在该控制器的构造函数中包含以下语句:
$scope.currentUserId = Person.getCurrentId();

就这么简单。添加” 人员” 作为你控制器的依赖项(如果尚未添加)。
索引数据库 IndexedDB是用于在浏览器中存储大量数据的更新工具。你可以使用它来存储任何JavaScript类型的数据, 例如对象或数组, 而无需对其进行序列化。针对数据库的所有请求都是异步的, 因此在请求完成后你将获得回调。
你可以使用IndexedDB来存储与服务器上的任何数据都不相关的结构化数据。例如日历, 待办事项列表或在本地玩的已保存游戏。在这种情况下, 该应用程序实际上是本地应用程序, 而你的网站只是提供该应用程序的工具。
当前, Internet Explorer和Safari仅部分支持IndexedDB。其他主要的浏览器完全支持它。但是, 目前的一个严重限制是Firefox完全在私有浏览模式下禁用了IndexedDB。
作为使用IndexedDB的具体示例, 让我们以PavolDani?的滑动拼图应用程序为例, 并对其进行调整以保存第一个拼图的状态, 该动作是每次??移动后基于AngularJS徽标的Basic 3× 3滑动拼图。重新加载页面将恢复第一个难题的状态。
我用这些更改设置了存储库的分支, 所有这些更改都在app / js / puzzle / slidingPuzzle.js中。如你所见, IndexedDB的基本用法也涉及其中。我将在下面显示重点。首先, 在页面加载期间调用函数restore来打开IndexedDB数据库:
/* * Tries to restore game */ this.restore = function(scope, storekey) { this.storekey = storekey; if (this.db) { this.restore2(scope); } else if (!window.indexedDB) { console.log('SlidingPuzzle: browser does not support indexedDB'); this.shuffle(); } else { var self = this; var request = window.indexedDB.open('SlidingPuzzleDatabase'); request.onerror = function(event) { console.log('SlidingPuzzle: error opening database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onupgradeneeded = function(event) { event.target.result.createObjectStore('SlidingPuzzleStore'); }; request.onsuccess = function(event) { self.db = event.target.result; self.restore2(scope); }; } };

request.onupgradeneeded事件处理数据库尚不存在的情况。在这种情况下, 我们创建对象存储。
打开数据库后, 将调用函数restore2, 该函数将查找具有给定键的记录(在这种情况下, 该记录实际上是常量” Basic” ):
/* * Tries to restore game, once database has been opened */ this.restore2 = function(scope) { var transaction = this.db.transaction('SlidingPuzzleStore'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var self = this; var request = objectStore.get(this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error reading from database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onsuccess = function(event) { if (!request.result) { console.log('SlidingPuzzle: no saved game for ' + self.storekey); scope.$apply(function() { self.shuffle(); }); } else { scope.$apply(function() { self.grid = request.result; }); } }; }

如果存在这样的记录, 则其值将替换难题的网格数组。如果在恢复游戏时出现任何错误, 我们只需像以前一样对游戏块进行洗牌。请注意, grid是3× 3的平铺对象数组, 每个对象都相当复杂。 IndexedDB的最大优点是你可以存储和检索这些值而无需序列化它们。
我们使用$ apply通知AngularJS模型已更改, 因此将适当更新视图。这是因为更新是在DOM事件处理程序中进行的, 因此AngularJS无法检测到更改。因此, 任何使用IndexedDB的AngularJS应用程序都可能需要使用$ apply。
在执行任何会更改网格数组的操作(例如用户的移动)之后, 将调用函数save, 该函数将基于更新的网格值使用适当的键来添加或更新记录:
/* * Tries to save game */ this.save = function() { if (!this.db) { return; } var transaction = this.db.transaction('SlidingPuzzleStore', 'readwrite'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var request = objectStore.put(this.grid, this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error writing to database, ' + request.error.name); }; request.onsuccess = function(event) { // successful, no further action needed }; }

其余的更改是在适当的时候调用上述函数。你可以查看显示所有更改的提交。请注意, 我们仅将恢复称为基本难题, 而不是针对三个高级难题。我们利用三个高级拼图均具有api属性这一事实, 因此对于那些拼图, 我们只进行常规混洗。
如果我们也想保存和还原高级拼图怎么办?这将需要进行一些重组。在每个高级拼图中, 用户可以调整图像源文件和拼图尺寸。因此, 我们必须增强存储在IndexedDB中的值以包含此信息。更重要的是, 我们需要一种从还原中更新它们的方法。对于这个已经冗长的示例来说, 这有点多了。
总结 在大多数情况下, 网络存储是存储会话数据的最佳选择。所有主流浏览器都完全支持它, 并且它的存储容量比cookie大得多。
如果已经将服务器设置为使用Cookie, 或者需要在所有窗口的所有选项卡之间都可以访问数据, 则可以使用Cookie, 但是你还想确保在关闭浏览器时将其删除。
你已经使用片段标识符来存储特定于该页面的会话数据, 例如用户正在查看的照片的ID。尽管你可以将其他会话数据嵌入片段标识符中, 但与网络存储或Cookie相比, 这实际上没有任何优势。
【跨页面重新加载的持久数据(Cookie,IndexedDB及其之间的所有内容)】与其他任何技术相比, 使用IndexedDB可能需要更多的编码。但是, 如果你要存储的值是难以序列化的复杂JavaScript对象, 或者你需要事务模型, 那么这可能是值得的。

    推荐阅读