一篇不错的了解Backbone的文章

来源:互联网 发布:点击显示隐藏的div js 编辑:程序博客网 时间:2024/04/29 07:32

【声明】此文章为转载,文章源地址:http://www.starming.com/index.php?action=plugin&v=wave&tpl=union&ac=viewgrouppost&gid=119&tid=11205


概览¶

&yet 逐渐有了越来越多的富 JS 应用。直到最近,我们才找到合适通用的应用架构。

并不出奇,我们发现要重复处理很多相同的问题。

我们在服务端通过 django 来实现 MVC 结构,但客户端代码里却没有清晰的结构。有些大的类库有此功能,但通常会附加上非常多的组件。比如 Sproutcore, YUI 和 Google Closure, 以及类似 GWT 和 Cappuccino 这样把其他语言编译成 JS 的工具包。

但对于我们这群希望在 JavaScript 里手动打造 UI 且对需要到实现知根知底的人来说,仍然渴望能有快速轻量的解决方案,而这些工具包则感觉有点过。

最近,我看到有个叫 Backbone.js 的库,并仔细看了下。结果我被他征服了,接下去会解释为什么用他以及如何用。

问题¶

创建复杂的应用肯定会面临很多挑战,首先是要管理日益庞大的代码。此外,由于 JavaScript 没有正式的类,因此也没有明确的方法来组织整个应用。

因为这些原因,JS 开发新人创建类似应用通常会经历类似以下过程:

  1. 对 jQuery 等类库很兴奋,尝试在 DOM 中存储数据和应用状态
  2. 需要记录的内容越来越多,开始觉得第一步很 tricky,尝试在 JS model 中存储状态
  3. 开始发现绑定 model change 到 UI,通过 model 的 setter 和 getter 调用函数,会变得比较混乱。不得不在 UI 上重复 model 的修改,而且还不清楚在哪里改,于是写出很多意大利面条式的代码
  4. 开始构建自己的应用框架甚至基础类库
  5. 最后发现,上面的痛苦,早就有人经历过,并解决了,而且开源了,维护得很好,代码质量很好,社区的人都很聪明

目标¶

那么,我们期望应用是什么样?以下是我觉得比较完美的组织:

  1. 所有 state 和 model 放在同一个地方
  2. model 的任何调整自动同步到 UI (不管有几个地方需要同步)
  3. 清晰可维护的代码结构
  4. 尽可能少的 “耦合代码”

开始 Backbone.js¶

Backbone 没有提供组件或应用,甚至没有提供任何视图层的东西。他只提供了一些关键对象来辅助代码的组织,即:Models, Collections 和 Views。最终是为了在客户端创建清晰的 MVC 应用。此外,还有些基础对象以及处理 change 的事件架构。我们来依次看下。

MODEL 对象¶

model 对象可用来设置和获取属性。创建他只需这样:

var Movie = Backbone.Model.extend({}); 

现在你可以进行实例化,设置和获取属性等操作。

matrix = new Movie();matrix.set({    title: "The Matrix",    format: "dvd'});matrix.get('title');

还可以在实例化时直接传入属性,如:

matrix = new Movie({    title: "The Matrix",    format: "dvd'});

如果需要在实例化时强制创建特定属性,可提供 initialize() 函数来做初始化检查。基于约定,initialize 函数被调用时会传入你传给构造器的参数。

var Movie = Backbone.Model.extend({     initialize: function (spec) {         if (!spec || !spec.title || !spec.format) {             throw "InvalidConstructArgs";         }          // we may also want to store something else as an attribute         // for example a unique ID we can use in the HTML to identify this         // item's element. We can use the models 'cid' or 'client id for this'.         this.set({             htmlId: 'movie_' + this.cid         })     } }); 

还可以定义 validate() 方法。他会在设置属性时被调用,可以用来验证属性。只要 validate()方法返回任何内容都会阻止属性的设置。

var Movie = Backbone.Model.extend({     validate: function (attrs) {         if (attrs.title) {             if (!_.isString(attrs.title) || attrs.title.length === 0 ) {                 return "Title must be a string with a length";             }         }     } }); 

model 中还有其他非常多的好东西。这里我只做概括,并没有代替原生文档的意思。。我们继续。

COLLECTIONS¶

collection 是某一类型 model 的有序集合。他除了把 model 储存在一个 JS 数组里,还提供了一堆实用功能。比如获取 model 时,可通过 comparator() 函数定义规则来保持 model 的有序返回。

先 collection 要保存什么类型的 model,之后的事情就非常简单了:

// define our collection var MovieLibrary = Backbone.Collection.extend({     model: Movie,     initialize: function () {         // somthing     } });  var library = new MovieLibarary();  // you can add stuff by creating the model first var dumb_and_dumber = new Movie({     title: "Dumb and Dumber",     format: "dvd" });  library.add(dumb_and_dumber);  // or even by adding the raw attributes library.add({     title: "The Big Lebowski",     format: "VHS" }); 

同上,collection 中也还有很多其他好东西,但主要功能是解决有序维护 model 集合的问题。

VIEWS¶

在这里可以操作 DOM ,以及处理一些兼容性问题。这是唯一于依赖 jQuery 的地方。

通常约定用 view 来向浏览器绘制 model 的改变,可以直接操作 HTML。初始渲染时(首次添加新的model)还需要一些给力的客户端模板引擎。我个人倾向于使用 ICanHaz.js 和 Mustache.js 。(如果对此感兴趣,可以看 ICanHaz.js on github) 此时,view 就可以监听并响应 model 的修改了。

这里有个 Movie 对应的简单的 view:

var MovieView = Backbone.View.extend({     initialize: function (args) {         _.bindAll(this, 'changeTitle');         this.model.bind('change:title', this.changeTitle);     },      events: {         'click .title': 'handleTitleClick'     },      render: function () {         // "ich" is ICanHaz.js magic         this.el = ich.movie(this.model.toJSON());         return this;     },      changeTitle: function () {         this.$('.title').text(this.model.get('title'));     },      handleTitleClick: function () {         alert('you clicked the title: ' + this.model.get('title'));     } }); 

view 处理两类事件。首先, events 属性用于连接用户事件和处理器,这个例子里具体是处理模板中包含 title class 元素的点击事件。另外,backbone 还提供很多强大的功能来确保任何对于 model 的修改会自动更新到 html。

合到一起¶

目前为止,我们谈论的是一些片段。现在我们来说下如何把他们汇聚到一起,做一个完整的应用。

全局 CONTROLLER 对象¶

虽然也可以把 controller 放在 AppView 对象里,但我不喜欢把 model 对象存在 view 里。所以我创建了一个全局的 controller 对象来存所有的东西。我创建了一个名字和应用相同的简单的单例对象,继续我们的例子:

var MovieAppController = {     init: function (spec) {         // default config         this.config = {             connect: true         };         // extend our default config with passed in object attributes         _.extend(this.config, spec);         this.model = new MovieAppModel({             nick: this.config.nick,             account: this.config.account,             jid: this.config.jid,             boshUrl: this.config.boshUrl         });         this.view = new MovieAppView({model: this.model});         // standalone modules that respond to document events         this.sm = new SoundMachine();         return this;     },      // any other functions here should be events handlers that respond to     // document level events. In my case I was using this to respond to incoming XMPP     // events. So the logic for knowing what those meant and creating or updating our     // models and collections lived here.     handlePubSubUpdate: function () {}; }; 

可以看到我们有一个应用 model,包含所有其他 model, collection 和应用 view。

这个例子中的应用 model 包含了所有的 collection,以及应用 view 可能要用到的属性:

var MovieAppModel = Backbone.Model.extend({     initialize: function () {         // init and store our MovieCollection in our app object         this.movies = new MovieCollection();     } }); 

应用 view 可能是这样:

var MovieAppView = Backbone.View.extend({     initialize: function () {         // this.model refers the the model we pass to the view when we         // first init our view. So here we listen for changes to the movie collection.         this.model.movies.bind('add', this.addMovie);         this.model.movies.bind('remove', this.removeMovie);     },      events: {         // any user events (clicks etc) we want to respond to     },      // grab and populate our main template     render: function () {         // once again this is using ICanHaz.js, but you can use whatever         this.el = ich.app(this.model.toJSON());         // store a reference to our movie list         this.movieList = this.$('#movieList');         return this;     },      addMovie: function (movie) {         var view = new MovieView({model: movie});         // here we use our stored reference to the movie list element and         // append our rendered movie view.         this.movieList.append(view.render().el);     },      removeMovie: function (movie) {         // here we can use the html ID we stored to easily find         // and remove the correct element/elements from the view if the         // collection tells us it's been removed.         this.$('#' + movie.get('htmlId')).remove();     } }); 

现在来看整个应用。我引入了全部依赖的文件,以及 ICanHaz.js 模板引擎。然后在$(document).ready() 里调用 init 函数并传入一些服务端过来的变量,用于模板替换。最后调用 render() 方法渲染应用 view 到 <body> 元素,比如这样:

<!DOCTYPE html><html>    <head>        <title>Movies App</title>         <!-- libs -->        <script src="jquery.js"></script>        <script src="underscore.js"></script>        <script src="backbone.js"></script>         <!-- client templating -->        <script src="mustache.js"></script>        <script src="ICanHaz.js"></script>         <!-- your app -->        <script src="Movie.js"></script>        <script src="MovieCollection.js"></script>        <script src="MovieView.js"></script>        <script src="MovieAppModel.js"></script>        <script src="MovieAppView.js"></script>        <script src="MovieAppController.js"></script>         <!-- ICanHaz templates -->        <script id="app" type="text/html">            <h1>Movie App</h1>            <ul id="movies"></ul>        </script>        <script id="movie" type="text/html">            <li id="{{ htmlId }}"><span class="title">{{ title }}</span> <span>{{ format }}</span></li>        </script>         <script>            $(document).ready(function () {                // init our app                window.app = MovieAppController.init({                    account: '{{ user.get_profile.account.slug }}',                    // etc, etc                });                // draw our main view                $('body').append(app.view.render().el);            });        </script>     </head>    <body></body></html>

都做完了,现在从 collection 中添加或移除 movie 或者改变 model 里的 title,这些修改都会通过 MovieView 中定义的方法反映到 HTML 中,非常神奇。

通用技巧¶

  1. 开发环境,所有对象按文件存放
  2. 生产环境,压缩合并所有的 JS 到一个文件
  3. 使用 JSLint
  4. 在 underscore.js 里扩展实用方法,而不是修改原生对象 (详见此 gist)
  5. jQuery 应该只在 view 里实用,这样可以在服务端对 model 和 collection 进行测试
  6. 用普通的 jQuery 事件来注册其他 view,避免强耦合
  7. 尽量保持 model 的简单

我知道我写了很多,而且有些内容和 backbone.js 上的文档 重复。但要基于 backbone.js 创建应用时,可能会希望有像这样更高层次的概括,所以我写了这篇文章。感谢 DocumentCloud 和Jeremy Ashkenas 创建并分享了 backbone 。

如果有任何想法,欢迎通过 twitter (@HenrikJoreteg) 联系我。谢谢!