w3ctech

本地数据存储与同步

早先的Web程序,是脱离不了网络运行的。所有的数据,包括图片等静态资源,每一次都要向服务器请求。在局域网的情况下,这通常不是什么大问题。但现在全面地移动化了,网络环境日趋复杂,再加上互联网的应用面越来越广,原来的小烦恼,现在越来越重要了。中国还算好,移动通信的覆盖面广,连秦岭之巅都有电信的3G信号,但仍然有隧道、室内死角等网络不通畅的情况。应用设计之初,就得考虑本地存储,而不是把它当作异常来处理。

本地存储的第一步,是缓存。把服务器取来的页面和数据,暂时存一部分在本地。一方面,不需要反复地向服务器请求,减少负载;另一方面,万一服务器访问不到了,打开时还能看到。浏览器自动地为我们做了一些缓存的工作,静态资源可以手工指定用AppCache缓存。而数据的缓存,只能通过代码来实现了。这一点,稍微用点心的应用,都做到了。像微博、知乎的客户端,断网时打开,都可以看到之前浏览过的内容。

再进一步,是在离线的情况下,修改的东西不会丢。比如发一条新微博,如果网络不通畅,自动存在本地,简单地提示你一下。等有网了,自动重发,或者手工点一下重发。像QQ、微信和微博都有这样的功能。

资深一点的网民都有过这样的经历:泡论坛的时候,辛辛苦苦写的一大段文字,一提交,半天连不上,最后网页无法显示。退回来,东西已经没有了,砸电脑的心都有。被虐久了就养成习惯,开个记事本把内容先存起来,发失败了,我这里还有。这其实是应用本身需要解决的问题。

转过头来看看数据的同步。

首先是跨终端的同步。数据一保存到服务器,不就同步了么?但别忘了,其他人还在看呢。两个人开同一个网页,这边更新了个东西,在另一个页面需不需要立即反映出来?或者说后台通过计算,或者与第三方的接口,自动更新了一些数据,页面上也要能很快地显示出来。这样,无论你一个界面开了多久,都能看到实时的情况,而无需刷新。如果用户是各干各的事儿就用不上,但那些监控的、协作的、社交的应用,就特别需要。以后恐怕难以找到不需要同步的了。

在用户眼里,你的应用就应该是一个整体,这边剪切,那边粘贴,这边更改,那边显示。不同的终端,只是呈现而已。产品应该让用户直接地达到使用的目的,而不需要做一些奇怪的事情,比如点击“保存”或者“刷新”。

这需要服务器有个协调作用,一个用户的修改,知道还该发给谁。而其它的终端,要能够及时获得通知。定时去取最简单,但频率高了太消耗服务器和带宽资源,频率低了通知又不及时。所以会用Long Polling, WebSockets等方法,而SocketJS则在不支持WebSockets的环境下为开发者屏蔽了细节。

然后是协作编辑。比较容易的办法,就是加锁,只允许一个session编辑,其它的都只能读。比如Tower的文档编辑就是这么做的。这跟数据库的加锁机制类似,有表级锁,有行级锁,有共享锁,有排他锁。虽然这只是规避,但有时简单一点的方法反倒足够好用。而像Google Docs这样全功能的办公套件,就需要考虑很多复杂的应用场景。

如果支持离线,要考虑的事情更多了。最简单的仲裁方式是按修改时间。但如果两个终端都离线修改了数据,连上网后,到底以修改的时间,还是服务器的时间?如果两台终端的本地时间有偏差,怎么办?需要具体解决的问题。很多时候,往往要避开这个问题。

下面说说我找到的一些工具。

PouchDB。它首先把本地存储的几种方式的细节屏蔽掉,不同的浏览器选择不同的类型。更进一步,是能自动同步到服务器上的CouchDB。

使用PouchDB的代码大致是这样的:

var db = new PouchDB('dbname');

db.put({
   _id: 'dave@gmail.com',
   name: 'David',
   age: 67
});

db.changes().on('change', function() {
   console.log('Ch-Ch-Changes');
});

db.replicate.to('http://example.com/mydb');

同步方面的工具很多。完全开源的Derby JS/Racer JS是比较模块化的,后端通过Adaptor可以支持各种数据存储。它本身的冲突解决又是基于ShareJS,一种Operational Transformation的实现,而后者是Google Docs得以做到协同编辑的基础。

而与其同时期的Meteor,2012年拿到了千万美元的投资,推广方面做得好一些,用起来傻瓜化的同时却又比较封闭,但它的后端只支持MongoDB,冲突解决也只能基于时间。

另外有很多在线的服务,像FirebaseDeployd等,以NoBackend为目标,让App开发者无需搭建自己的服务器就保存每个用户的信息。它们通常能做到实时的共享,短暂断网时的自动重发,但冲突的解决比较简单。当然,复杂的应用,第三方的服务通常也难以达到要求。

下面是Firebase的一个使用示例:

//CREATE A FIREBASE
var fb = new Firebase("https://YOUR.firebaseio.com/");

//SAVE DATA
fb.set({ name: "Alex Wolfe" });

//LISTEN FOR REALTIME CHANGES
fb.on("value", function(data) {
  var name = data.val() ? data.val().name : "";
  alert("My name is " + name);
});

这些工具通常只做到了少量的离线支持。数据是放在内存里的,如果浏览器没有关,或者App没退出,连上网之后,会自动同步。但一旦退出,数据就丢失了。

最近新的一个Hoodie,提出了一个叫Offline First的理念。把离线和同步两个东西融合起来了。数据先存在Local Storage,联上网就会自动同步到服务器,服务器又会通知其它终端,同步过去。不久它将使用PouchDB作为本地数据存储,以扩展自己的适用范围。

hoodie = new Hoodie();
hoodie.store.findAll().done(function(pets) {
$scope.pets = pets;
});

hoodie.store.on('change', function (eventName, changedObject) {
$scope.$apply(refresh);
});
$scope.addPet = function() {
hoodie.store.add('pets', {name: $scope.newPetName})
.done(function(newPet) {
// do nothing
});
};

$scope.removePet = function(index) {
var pet = $scope.pets[index];
hoodie.store.remove('pets', pet.id)
.done(function() {
// do nothing
});
};

但对于AngularJS的应用,有一个Plugin,可以让三方同步变得更简单:

hoodieArray.bind($scope, 'pets', 'pets');

// another controller
hoodie.store.on('change:pets', function (eventName, changedObject) {
  // ...
});

Offline First其实还带来一些附加的好处,比如服务器停机带来的影响更小。本来一停机,所有业务就受影响。但现在经过良好设计,应用都缓存到本地,除了别人的更新短时间看不到之外,本地的操作丝毫不受影响。

基于这篇文章,2014年6月在W3C Tech重庆站上做了分享,查看分享的Slides

w3ctech微信

扫码关注w3ctech微信公众号

共收到3条回复

  • 支持学习!

    回复此楼
  • 好分享,好文章。

    回复此楼
  • 不错的分享。 场景应该是打开应用后中途离线对吧,这里应该绑定离线事件。

    window.addEventListener("offline", function(e) { 
        console.log("Offline"); 
    }, true);
    
    回复此楼