[翻译]用 Backbone.js, underscore.js 和 jQuery 创建页面应用
by chencheng
at 2010-12-29 13:22:33
original http://www.chencheng.org/blog/2010/12/29/building-a-single-page-app-with-backbonejs-undersc/
原文:
概览
&yet 逐渐有了越来越多的富 JS 应用。直到最近,我们才找到合适通用的应用架构。
并不出奇,我们发现要重复处理很多相同的问题。
最近,我看到有个叫 的库,并仔细看了下。结果我被他征服了,接下去会解释为什么用他以及如何用。
问题¶
创建复杂的应用肯定会面临很多挑战,首先是要管理日益庞大的代码。此外,由于 JavaScript 没有正式的类,因此也没有明确的方法来组织整个应用。
因为这些原因,JS 开发新人创建类似应用通常会经历类似以下过程:
- 对 jQuery 等类库很兴奋,尝试在 DOM 中存储数据和应用状态
- 需要记录的内容越来越多,开始觉得第一步很 tricky,尝试在 JS model 中存储状态
- 开始发现绑定 model change 到 UI,通过 model 的 setter 和 getter 调用函数,会变得比较混乱。不得不在 UI 上重复 model 的修改,而且还不清楚在哪里改,于是写出很多意大利面条式的代码
- 开始构建自己的应用框架甚至基础类库
- 最后发现,上面的痛苦,早就有人经历过,并解决了,而且开源了,维护得很好,代码质量很好,社区的人都很聪明
目标¶
- 所有 state 和 model 放在同一个地方
- 清晰可维护的代码结构
开始 Backbone.js¶
Backbone 没有提供组件或应用,甚至没有提供任何视图层的东西。他只提供了一些关键对象来辅助代码的组织,即:Models, Collections 和 Views。最终是为了在客户端创建清晰的 MVC 应用。此外,还有些基础对象以及处理 change 的事件架构。我们来依次看下。
MODEL 对象¶
现在你可以进行实例化,设置和获取属性等操作。
matrix = new Movie(); matrix.set({ title: "The Matrix", format: "dvd' }); matrix.get('title');
如果需要在实例化时强制创建特定属性,可提供 initialize() 函数来做初始化检查。基于约定,initialize 函数被调用时会传入你传给构造器的参数。
还可以定义 方法。他会在设置属性时被调用,可以用来验证属性。只要 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=""><span class="title"></span> <span></span></li> </script> <script> $(document).ready(function () { // init our app window.app = MovieAppController.init({ account: '', // etc, etc }); // draw our main view $('body').append(app.view.render().el); }); </script> </head> <body></body> </html>
都做完了,现在从 collection 中添加或移除 movie 或者改变 model 里的 title,这些修改都会通过 MovieView 中定义的方法反映到 HTML 中,非常神奇。
通用技巧¶
- 开发环境,所有对象按文件存放
- 生产环境,压缩合并所有的 JS 到一个文件
- 使用 JSLint
- 在 underscore.js 里扩展实用方法,而不是修改原生对象 (详见此 gist)
- jQuery 应该只在 view 里实用,这样可以在服务端对 model 和 collection 进行测试
- 用普通的 jQuery 事件来注册其他 view,避免强耦合
- 尽量保持 model 的简单
我知道我写了很多,而且有些内容和 backbone.js 上的文档 重复。但要基于 backbone.js 创建应用时,可能会希望有像这样更高层次的概括,所以我写了这篇文章。感谢 DocumentCloud 和 Jeremy Ashkenas 创建并分享了 backbone 。
如果有任何想法,欢迎通过 twitter (@HenrikJoreteg) 联系我。谢谢!