即将到来的 SharedWorker API 能够在 iframe 甚至浏览器标签或窗口中传输数据。它在几年前就已在 Chrome 中得以实现,不久前也在 Firefox 上实现了,不过它在 IE 和 Safari 中仍然难觅踪影。还好,这个 API 有一种拥有广泛浏览器支持,但鲜为人知的替代方案。是时候探索它了!
现在,我需要对以下应用情景找到一个优雅的解决方案:假设有个人访问了你的网站。他依次登录,打开第二个标签页并在那个标签页里选择了注销。这时,他所打开的第一个标签页看起来仍然保留着「已登录」的状态,但这时他的所有操作要么会重定向到登录页面,要么会直接让他抓狂。更吸引人的解决方式则是判断用户是否已注销,并对页面做相应的改变。譬如可以显示一个对话框来提示用户需要重新验证,或者显示原本的登录视图。
这个功能可以通过 WebSocket API 来实现,不过这就有些小题大做了。毕竟杀鸡焉用牛刀,于是我开始寻找一些其它的跨标签页通信方式。我首先想到的就是使用 cookies 或者 localStorage
,来周期性地通过 setInterval
检查用户是否登录。对这个方案我并不满意,因为这样会把许多 CPU 周期耗费在检查一个可能自始至终都不会满足的条件上。这时候我就觉得还不如就直接用 “comet”(又名轮询)、服务器端事件或者 WebSockets 算了呢。
所以当我发现自己是在骑驴找驴的时候还是很吃惊,因为答案就是一直以来的 localStorage
!
你知道 localStorage
会触发一个事件吗?具体地说,不论其中的哪一项在另一个浏览上下文里被添加、修改或删除时,它都会触发一个事件。实际上,这就意味着不论在哪个浏览器的标签页里访问了 localStorage
,所有其它的标签页都能通过 window
对象监听到这个事件,就像这样:
1 2 3 |
window.addEventListener('storage', function (event) { console.log(event.key, event.newValue); }); |
event
对象有几个相应的属性:
属性 | 描述 |
---|---|
key |
localStroage 中被影响的键 |
newValue |
为这个键所赋的新值 |
oldValue |
这个键修改前的值 |
url |
当前发生改变的页面 URL |
不论某个标签页在何时修改了 localStorage
,都会对其余的所有标签触发事件。这就意味着我们只要为 localStorage
赋值,就能够跨浏览器标签通信了。请看下面伪代码风格的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
var loggedOn; // TODO: call when logged-in user changes or logs out logonChanged(); window.addEventListener('storage', updateLogon); window.addEventListener('focus', checkLogon); function getUsernameOrNull () { // TODO: return whether the user is logged on } function logonChanged () { var uname = getUsernameOrNull(); loggedOn = uname; localStorage.setItem('logged-on', uname); } function updateLogon (event) { if (event.key === 'logged-on') { loggedOn = event.newValue; } } function checkLogon () { var uname = getUsernameOrNull(); if (uname !== loggedOn) { location.reload(); } } |
大意就是当用户打开了两个标签页,在其中一个里执行了注销操作后返回另一个时,页面将重新载入,(如果可以的话)服务器端逻辑将把用户重定向到其它位置。这个检查只在当前标签页获得焦点时执行,这是因为用户可能在注销后立刻重新登录,这种情况下不应将其余标签页的状态全部设为已注销。
这段代码肯定还可以改进,不过它已经很好地满足了需求。更好的实现方式可能会立刻要求用户登录,但要注意它也可能以相反的方式来工作:用户登录后打开另一个已经注销的标签页时,代码会检查并重新载入页面,然后服务器(再说一遍,如果可以的话)就可以把用户重定向到登录页面的不老泉里,期盼着你能有一次打电话给这个网站的经验。
更简单的 API
localStorage
API 可以说是 web 浏览器最简单的 API 之一了,并且它还享有相当不错的跨浏览器支持。不过,一些浏览器的仍然存在着 quirks,譬如无痕模式下的 Safari 在设置值时会抛出 QuotaExceededError
的异常,有某些浏览器不支持开箱即用的 JSON,还有一些旧版浏览器会让你感到沮丧。
因此,我整合了一个 local-storage 模块,为 localStorage
提供了简化的 API,从而摆脱这些 quirks,在缺少 localStorage
API 时会回退到内存存储,并通过使你为特定键注册或取消注册监听器,使得对 storage
事件的使用更加容易。
截止到写这篇文章时,local-storage@1.3.1
中最新的 API 端点(译者注:2015-01-08) 如下:
ls(key, value?)
取得或设置键ls.get(key)
取得键的值ls.set(key, value)
为键指定值ls.remove(key)
移除键ls.on(key, fn(value, old, url))
监听其它标签页的键值改变并触发 fnls.off(key, fn)
取消之前使用ls.on
注册的监听器
同样值得一提的是, local-storage 注册了一个单一的 storage
对象处理器并保持你对每个键的跟踪,而不是注册多个 storage
对象。
我对学习其它跨标签页通信的底层实现方式也很有兴趣!它对离线优先的开发很有帮助,尤其是考虑到当前 SharedWorker
还需要一段时间才能迎来广泛支持,而 WebSocket 在离线使用的情境下也靠不住时,这种通信方式就更有意义了。