BACKBONE.JS 简单入门范例
11年刚开始用前端mvc框架时写过一篇文章,当时knockout和backbone都在用,但之后的项目全是在用backbone,主要因为它简单、灵活,无论是富js应用还是企业网站都用得上。相比react针对view和单向数据流的设计,backbone更能体现mvc的思路,所以针对它写一篇入门范例,说明如下:
1. 结构上分4节,介绍model/view/collection,实现从远程获取数据显示到表格且修改删除;
2. 名为“范例”,所以代码为主,每节的第1段代码都是完整代码,复制粘贴就能用,每段代码都是基于前一段代码来写的,因此每段代码的新内容不会超过20行(大括号计算在内);
3. 每行代码没有注释,但重要内容之后有写具体的说明;
4. 开发环境是chrome,使用github的api,这样用chrome即使在本地路径(形如file://的路径)也能获取数据。
0. introduction
几乎所有的框架都是在做两件事:一是帮你把代码写在正确的地方;二是帮你做一些脏活累活。backbone实现一种清晰的mvc代码结构,解决了数据模型和视图映射的问题。虽然所有js相关的项目都可以用,但backbone最适合的还是这样一种场景:需要用js生成大量的页面内容(html为主),用户跟页面元素有很多的交互行为。
backbone对象有5个重要的函数,model/collection/view/router/history。router和history是针对web应用程序的优化,建议先熟悉pushstate的相关知识。入门阶段可以只了解model/collection/view。将model视为核心,collection是model的集合,view是为了实现model的改动在前端的反映。
1. model
model是所有js应用的核心,很多backbone教程喜欢从view开始讲,其实view的内容不多,而且理解了view意义不大,理解model更重要。以下代码实现从github的api获取一条gist信息,显示到页面上:
<!doctype html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>url</th><th>created_at</th></thead> <tbody></tbody> </table> <script type="text/javascript"> var gist = backbone.model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return (response[0]); } }), gist = new gist(); gist.on('change', function (model) { var tbody = document.getelementbyid('js-id-gists').children[1], tr = document.getelementbyid(model.get('id')); if (!tr) { tr = document.createelement('tr'); tr.setattribute('id', model.get('id')); } tr.innerhtml = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td>'; tbody.appendchild(tr); }); gist.fetch(); </script> </body> </html>
line4~8: 加载要用到的js库。ajax请求和部分view的功能需要jquery支持(或者重写ajax/view的功能);backbone的代码是基于underscore写的(或者用lo-dash代替);加载bootstrap.css只是因为默认样式太难看…
line16~22: 创建一个model并实例化。url是数据源(api接口)的地址,parse用来处理返回的数据。实际返回的是一个array,这里取第一个object。
line24~33: 绑定change事件。还没有使用view,所以要自己处理html。这10行代码主要是get的用法(model.get),其他的功能之后会用view来实现。
line34: 执行fetch。从远程获取数据,获到数据后会触发change事件。可以重写sync方法
打开chrome的console,输入gist,可以看到model获得的属性:
model提供数据和与数据相关的逻辑。上图输出的属性是数据,代码中的fetch/parse/get/set都是对数据进行操作,其他的还有escape/unset/clear/destory,从函数名字就大致可以明白它的用途。还有很常用的validate函数,在set/save操作时用来做数据验证,验证失败会触发invalid事件:
/* 替换之前代码的js部分(line16~34) */ var gist = backbone.model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return (response[0]); }, defaults: { website: 'dmyz' }, validate: function (attrs) { if (attrs.website == 'dmyz') { return 'website error'; } } }), gist = new gist(); gist.on('invalid', function (model, error) { alert(error); }); gist.on('change', function (model) { var tbody = document.getelementbyid('js-id-gists').children[1], tr = document.getelementbyid(model.get('id')); if (!tr) { tr = document.createelement('tr'); tr.setattribute('id', model.get('id')); } tr.innerhtml = '<td>'+ model.get('description') +'</td><td>'+ model.get('url') +'</td><td>'+ model.get('created_at') +'</td>'; tbody.appendchild(tr); }); gist.save();
跟之前的代码比较,有4处改动:
line7~9: 增加了defaults。如果属性中没有website(注意不是website值为空),会设置website值为dmyz。
line10~14: 增加validate函数。当website值为dmyz时,触发invalid事件。
line18~20: 绑定invalid事件,alert返回的错误。
line31: 不做fetch,直接save操作。
因为没有fetch,所以页面上不会显示数据。执行save操作,会调用validate函数,验证失败会触发invalid事件,alert出错误提示。同时save操作也会向model的url发起一个put请求,github这个api没有处理put,所以会返回404错误。
在console中输入gist.set(‘description', ‘demo'),可以看到页面元素也会有相应的变化。执行gist.set(‘description', gist.previous(‘description'))恢复之前的值。这就是model和view的映射,现在还是自己实现的,下一节会用backbone的view来实现。
2. view
用backbone的view来改写之前代码line24~33部分:
<!doctype html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>url</th><th>created_at</th><th></th></thead> <tbody></tbody> </table> <script type="text/javascript"> var gist = backbone.model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return response[0]; } }), gist = new gist(); var gistrow = backbone.view.extend({ el: 'tbody', model: gist, events: { 'click a': 'replaceurl' }, replaceurl: function () { this.model.set('url', 'http://dmyz.org'); }, initialize: function () { this.listento(this.model, 'change', this.render); }, render: function () { var model = this.model, tr = document.createelement('tr'); tr.innerhtml = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" >®</a></td>'; this.el.innerhtml = tr.outerhtml; return this; } }); var tr = new gistrow(); gist.fetch(); </script> </body> </html>
line25: 所有的view都是基于dom的,指定el会选择页面的元素,指定tagname会创建相应的dom,如果都没有指定会是一个空的div。
line27~32: 绑定click事件到a标签,replaceurl函数会修改(set)url属性的值。
line33~35: view的初始化函数(initialize),监听change事件,当model数据更新时触发render函数。
line36~42: render函数。主要是line41~42这两行,把生成的html代码写到this.el,返回this。
line44: 实例化gistrow,初始化函数(initialize)会被执行。
点击行末的a标签,页面显示的这条记录的url会被修改成http://dmyz.org。
这个view名为gistrow,选择的却是tbody标签,这显然是不合理的。接下来更改js代码,显示api返回的30条数据:
/* 替换之前代码的js部分(line16~45) */ var gist = backbone.model.extend(), gists = backbone.model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new gists(); var gistrow = backbone.view.extend({ tagname: 'tr', render: function (object) { var model = new gist(object); this.el.innerhtml = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>' return this; } }); var gistsview = backbone.view.extend({ el: 'tbody', model: gists, initialize: function () { this.listento(this.model, 'change', this.render); }, render: function () { var html = ''; _.foreach(this.model.attributes, function (object) { var tr = new gistrow(); html += tr.render(object).el.outerhtml; }); this.el.innerhtml = html; return this; } }); var gistsview = new gistsview(); gists.fetch();
line2~9: 创建了两个model(gist和gists),parse现在返回完整array而不只是第一条。
line11~18: 创建一个tr。render方法会传一个object来实例化一个gist的model,再从这个model里get需要的值。
line26~34: 遍历model中的所有属性。现在使用的是model而不是collection,所以遍历出的是object。foreach是underscore的函数。
backbone的view更多的是组织代码的作用,它实际干的活很少。view的model属性在本节第一段代码用的是大写,表明只是一个名字,并不是说给view传一个model它会替你完成什么,控制逻辑还是要自己写。还有view中经常会用到的template函数,也是要自己定义的,具体结合哪种模板引擎来用就看自己的需求了。
这段代码中的gists比较难操作其中的每一个值,它其实应该是gist的集合,这就是backbone的collection做的事了。
3. collection
collection是model的集合,在这个collection中的model如果触发了某个事件,可以在collection中接收到并做处理。第2节的代码用collection实现:
<!doctype html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>url</th><th>created_at</th><th></th></thead> <tbody></tbody> </table> <script type="text/javascript"> var gist = backbone.model.extend(), gists = backbone.collection.extend({ model: gist, url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new gists(); var gistrow = backbone.view.extend({ tagname: 'tr', render: function (model) { this.el.innerhtml = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>' return this; } }); var gistsview = backbone.view.extend({ el: 'tbody', collection: gists, initialize: function () { this.listento(this.collection, 'reset', this.render); }, render: function () { var html = ''; _.foreach(this.collection.models, function (model) { var tr = new gistrow(); html += tr.render(model).el.outerhtml; }); this.el.innerhtml = html; return this; } }); var gistsview = new gistsview(); gists.fetch({reset: true}); </script> </body> </html>
line17~23: 基本跟第2节的第2段代码一样。把model改成collection,指定collection的model,这样collectio获得返回值会自动封装成model的array。
line38: collection和model不同,获取到数据也不会触发事件,所以绑定一个reset事件,在之后的fetch操作中传递{reset: true}。
line42~45: 从collection从遍历model,传给gistrow这个view,生成html。
collection是backbone里功能最多的函数(虽然其中很多是underscore的),而且只要理解了model和view的关系,使用collection不会有任何障碍。给collection绑定各种事件来实现丰富的交互功能了,以下这段js代码会加入删除/编辑的操作,可以在jsbin上查看源代码和执行结果。只是增加了事件,没有什么新内容,所以就不做说明了,附上jsbin的演示地址:
/* 替换之前代码的js部分(line16~51) */ var gist = backbone.model.extend(), gists = backbone.collection.extend({ model: gist, url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new gists(); var gistrow = backbone.view.extend({ tagname: 'tr', render: function (model) { this.el.id = model.cid; this.el.innerhtml = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="js-remove">x</a> <a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="js-edit">e</a> </td>' return this; } }); var gistsview = backbone.view.extend({ el: 'tbody', collection: gists, events: { 'click a.js-remove': function (e) { var cid = e.currenttarget.parentelement.parentelement.id; gists.get(cid).destroy(); gists.remove(cid); }, 'click a.js-edit': 'editrow', 'blur td[contenteditable]': 'saverow' }, editrow: function (e) { var tr = e.currenttarget.parentelement.parentelement, i = 0; while (i < 3) { tr.children[i].setattribute('contenteditable', true); i++; } }, saverow: function (e) { var tr = e.currenttarget.parentelement, model = gists.get(tr.id); model.set({ 'description' : tr.children[0].innertext, 'url': tr.children[1].innertext, 'created_at': tr.children[2].innertext }); model.save(); }, initialize: function () { var self = this; _.foreach(['reset', 'remove', 'range'], function (e) { self.listento(self.collection, e, self.render); }); }, render: function () { var html = ''; _.foreach(this.collection.models, function (model) { var tr = new gistrow(); html += tr.render(model).el.outerhtml; }); this.el.innerhtml = html; return this; } }); var gistsview = new gistsview(); gists.fetch({reset: true});
afterword
虽然是入门范例,但因为篇幅有限,有些基本语言特征和backbone的功能不可能面面俱到,如果还看不懂肯定是我漏掉了需要解释的点,请(在google之后)评论或是邮件告知。
backbone不是jquery插件,引入以后整个dom立即实现增删改查了,也做不到knockoutjs/anglarjs那样,在dom上做数据绑定就自动完成逻辑。它是将一些前端工作处理得更好更规范,如果学习前端mvc的目的是想轻松完成工作,backbone可能不是最佳选择。如果有一个项目,100多行html和1000多行js,js主要都在操作页面dom(如果讨厌+号连接html还可以搭配react/jsx来写),那就可以考虑用backbone来重写了,它比其他庞大的mvc框架要容易掌握得多,作为入门学习也是非常不错的。
上一篇: JS实现div模块的截图并下载功能
下一篇: 前端构建工具之gulp的配置与搭建详解