一篇不错的了解Backbone的文章

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


概览

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

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

我们在服务端通过 django 来实现 MVC 结构,但客户端代码里却没有清晰的结构。有些大的类库有此功能,但通常会附加上非常多的组件。比如 SproutcoreYUI 和 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) 联系我。谢谢!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值