Xueqiu Engineering Blog

thoughts on creating xueqiu

用Backbone.js绑住服务端生成的html

| Comments

去年做雪球的timeline模块时我正深受 #newTwitter 的影响,倾向于把尽可能多的逻辑放到客户端去做,最后实现的时候选择了Backbone.js。使用Backbone.js的好处就不说了,这一两年它火的一塌糊涂,到处都是介绍的文章,而且这篇文章的重点也不是这个。

下面我假设您已经了解Backbone.js的作用和实现方式。

在页面初始化的时候,与发起一个ajax请求去取初始数据相比,把初始数据输出到页面里是一个更好的方案。Backbone.js提供了一个Loading Bootstrapped Models的FAQ,雪球也正是这样做的。把初始数据的json输出到页面里,然后Backbone.js用这个json来渲染页面。

但是这一年的实践中陆续发现一些问题:接口输出的timeline json里某些字段里偶尔出现一些不可见的换行符,导致浏览器解析json的时候出错。输出json字符串有injection可能(后来今年三月份的时候backbone特意在文档里加上了提示)。另外,随着业务复杂性的增长,接口直接输出的json体积在膨胀,很多属性已经不是页面展示所必须的,json的体积已经接近甚至已经超过了生成的html的体积。

同时我还在思考另外一个问题,backbone的使用场景其实是app,DocumentCloudTrello这种需要反复对页面元素操作,应用要处理好数据和UI的一致性,初始化的时候稍微慢一点也没有关系,用backbone再好不过了。但是雪球其实更像是page,用户打开页面希望尽早的看到数据,当然我们也需要经常操作页面的元素,也需要处理数据和UI的一致性问题。

在服务端把html就拼好然后传给浏览器似乎是最直接的答案。那么接下来就面临了这篇文章要处理的问题了,如果既用服务端渲染html,又能够继续使用现有的基于backbone的客户端程序,甚至可以随时切换渲染位置。

backbone的文档似乎没有明确的给出这样的建议,但是稍微思考一下backbone View的实现方式应该可以想到,既然view的events是绑在一个根el上面的,那么这个el是一个空的wrapper或者已经渲染好的html片段并不影响事件的delegate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var TimelineView = Backbone.View.extend({
  events: {
    "click .comment": "comment"
  }.
  comment: function(e){
   var $elm = $(e.target)
     , statusModel = this.collection.get($elm.data("id"))
   renderComment(statusModel)
  },
  render: function(){
    var timelineHtml = ''
    // some code to generate html from models
    $(this.el).html(timelineHtml)
  }
})
var statusCollection = new StatusCollection(statusList)
var timelineView = new TimelineView({
  el: $("#timeline"),
  collection: statusCollection
})
timelineView.render()

假设上面的一段代码是已有的客户端渲染的实现方式,需要说明的是statusList正是我们之前输出到页面里的timeline json,$("#timeline")是准备放timeline的wrapper。

现在改成服务端渲染之后发生的变化的后果,$("#timeline")变成了已经塞满status的列表,statusList不再存在。我们挨个解决。

$("#timeline")既然已经填满了,就不用再render啦。最后一行就改成了:

1
2
3
if ($("#timeline").html().trim()){
  timelineView.render()
}

statusList是空的,那么statusCollection也是空的,comment的时候就找不到status的model。本来作为json输出来的数据其实被塞到了dom里,那我们就应该找一个合适的时候把status model从dom里读出来。我选择在view初始化的时候获取,给TimelineView加上initialize方法。

1
2
3
4
5
6
7
8
9
10
var TimelineView = Backbone.View.extend({
  initialize: function(){
    if (this.collection.length) {
      var models = []
      // some code to read models from timeline dom
      this.collection.add(models)
    }
  },
  ...
})

好了,客户端的代码没有任何其他要改的了,所有的backbone的功能都会跟原来一样的工作,还可以吧?

最后完整的代码改造成了这样:

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 TimelineView = Backbone.View.extend({
  initialize: function(){
    if (this.collection.length) {
      var models = []
      // some code to read models from timeline dom
      this.collection.add(models)
    }
  },
  events: {
    "click .comment": "comment"
  }.
  comment: function(e){
   var $elm = $(e.target)
     , statusModel = this.collection.get($elm.data("id"))
   renderComment(statusModel)
  },
  render: function(){
    var timelineHtml = ''
    // some code to generate html from models
    $(this.el).html(timelineHtml)
  }
})
var statusCollection = new StatusCollection(statusList)
var timelineView = new TimelineView({
  el: $("#timeline"),
  collection: statusCollection
})
if ($("#timeline").html().trim()){
  timelineView.render()
}

后记

  • 这种处理对于需要SEO,或者特殊设备支持的应用来说更有意义。
  • 服务端渲染html会给服务端带来额外的cpu消耗,但是很小。不过我们还是做了适配,可以随时切换渲染方式。
  • 服务端渲染也需要模板,客户端渲染也需要模板,这个模板如何复用?对于使用node.js的雪球来说,很好处理。这个留给以后说。

Comments