设置这是我们将使用的文件结构:
css └── styles.cssjs └── collections └── todos.js └── models └── todo.js └── vendor └── backbone.js └── jquery-1.10.2.min.js └── underscore.js └── views └── app.js └── index.html
有一些东西是显而易见的,例如 /css/styles.css 和 /index.html。它们包含 css 样式和 html 标记。在 backbone.js 的上下文中,模型是我们保存数据的地方。因此,我们的待办事项将只是模型。因为我们将有多个任务,所以我们会将它们组织成一个集合。业务逻辑分布在视图和主应用程序文件 app.js 之间。 backbone.js 只有一个硬依赖项 - underscore.js。该框架也与 jquery 配合得很好,因此它们都转到 vendor 目录。我们现在只需要一点 html 标记,就可以开始了。
<!doctype html><html> <head> <title>my todos</title> <link rel=stylesheet type=text/css href=css/styles.css /> </head> <body> <div class=container> <div id=menu class=menu cf></div> <h1></h1> <div id=content></div> </div> <script src=js/vendor/jquery-1.10.2.min.js></script> <script src=js/vendor/underscore.js></script> <script src=js/vendor/backbone.js></script> <script src=js/app.js></script> <script src=js/models/todo.js></script> <script src=js/collections/todos.js></script> <script> window.onload = function() { // bootstrap } </script> </body></html>
正如您所看到的,我们将所有外部 javascript 文件都包含在底部,因为在 body 标记的末尾执行此操作是一个很好的做法。我们还正在准备应用程序的引导。有内容容器、菜单和标题。主导航是静态元素,我们不会更改它。我们将标题的内容和下面的div替换掉。
规划应用程序在我们开始做某事之前制定一个计划总是好的。 backbone.js 没有非常严格的架构,我们必须遵循它。这是该框架的好处之一。所以,在开始实现业务逻辑之前,我们先来谈谈基础。
命名空间一个好的做法是将代码放入其自己的范围内。注册全局变量或函数不是一个好主意。我们将创建的是一个模型、一个集合、一个路由器和几个 backbone.js 视图。所有这些元素都应该存在于私人空间中。 app.js 将包含包含所有内容的类。
// app.jsvar app = (function() { var api = { views: {}, models: {}, collections: {}, content: null, router: null, todos: null, init: function() { this.content = $(#content); }, changecontent: function(el) { this.content.empty().append(el); return this; }, title: function(str) { $(h1).text(str); return this; } }; var viewsfactory = {}; var router = backbone.router.extend({}); api.router = new router(); return api;})();
以上是揭示模块模式的典型实现。 api 变量是返回的对象,代表类的公共方法。 views、models 和 collections 属性将充当 backbone.js 返回的类的持有者。 content 是一个指向主用户界面容器的 jquery 元素。这里有两个辅助方法。第一个更新该容器。第二个设置页面的标题。然后我们定义了一个名为 viewsfactory 的模块。它将传递我们的视图,最后,我们创建了路由器。
你可能会问,为什么我们需要一个工厂来存储视图?嗯,使用 backbone.js 时有一些常见的模式。其中之一与视图的创建和使用有关。
var viewclass = backbone.view.extend({ /* logic here */ });var view = new viewclass();
最好只初始化视图一次并让它们保持活动状态。一旦数据发生更改,我们通常会调用视图的方法并更新其 el 对象的内容。另一种非常流行的方法是重新创建整个视图或替换整个 dom 元素。然而,从性能的角度来看,这并不是很好。因此,我们通常会得到一个实用程序类,它创建视图的一个实例并在需要时返回它。
组件定义我们有了一个命名空间,所以现在我们可以开始创建组件了。主菜单如下所示:
// views/menu.jsapp.views.menu = backbone.view.extend({ initialize: function() {}, render: function() {}});
我们创建了一个名为 menu 的属性,它保存导航的类。稍后,我们可以在工厂模块中添加一个方法来创建它的实例。
var viewsfactory = { menu: function() { if(!this.menuview) { this.menuview = new api.views.menu({ el: $(#menu) }); } return this.menuview; }};
上面是我们处理所有视图的方式,它将确保我们只获得同一个实例的一个。在大多数情况下,这种技术效果很好。
流程应用程序的入口点是 app.js 及其 init 方法。这就是我们将在 window 对象的 onload 处理程序中调用的内容。
window.onload = function() { app.init();}
之后,定义的路由器将获得控制权。根据 url,它决定执行哪个处理程序。在 backbone.js 中,我们没有通常的模型-视图-控制器架构。缺少控制器,大部分逻辑都放入视图中。因此,我们将模型直接连接到视图内的方法,并在数据更改后立即更新用户界面。
管理数据我们的小项目中最重要的是数据。我们的任务是我们应该管理的,所以让我们从那里开始。这是我们的模型定义。
// models/todo.jsapp.models.todo = backbone.model.extend({ defaults: { title: todo, archived: false, done: false }});
只有三个字段。第一个包含任务文本,另外两个是定义记录状态的标志。
框架内的所有东西实际上都是一个事件调度程序。由于模型是通过设置器更改的,因此框架知道数据何时更新,并可以通知系统的其余部分。一旦您将某些内容绑定到这些通知,您的应用程序就会对模型中的更改做出反应。这是 backbone.js 中一个非常强大的功能。
正如我一开始所说的,我们将有很多记录,我们将它们组织到一个名为 todos 的集合中。
// collections/todos.jsapp.collections.todos = backbone.collection.extend({ initialize: function(){ this.add({ title: learn javascript basics }); this.add({ title: go to backbonejs.org }); this.add({ title: develop a backbone application }); }, model: app.models.todo up: function(index) { if(index > 0) { var tmp = this.models[index-1]; this.models[index-1] = this.models[index]; this.models[index] = tmp; this.trigger(change); } }, down: function(index) { if(index < this.models.length-1) { var tmp = this.models[index+1]; this.models[index+1] = this.models[index]; this.models[index] = tmp; this.trigger(change); } }, archive: function(archived, index) { this.models[index].set(archived, archived); }, changestatus: function(done, index) { this.models[index].set(done, done); }});
initialize 方法是集合的入口点。在我们的例子中,我们默认添加了一些任务。当然,在现实世界中,信息将来自数据库或其他地方。但为了让您集中注意力,我们将手动执行此操作。集合的另一件事是设置 model 属性。它告诉类正在存储什么类型的数据。其余方法实现与我们应用程序中的功能相关的自定义逻辑。 up 和 down 函数更改 todos 的顺序。为了简化事情,我们将仅使用集合数组中的索引来标识每个 todo。这意味着如果我们想要获取一条特定记录,我们应该指向它的索引。所以,排序只是交换数组中的元素。正如您可能从上面的代码中猜到的那样, this.models 是我们正在讨论的数组。 archive 和 changestatus 设置给定元素的属性。我们将这些方法放在这里,因为视图将有权访问 todos 集合,而不是直接访问任务。
此外,我们不需要从 app.models.todo 类创建任何模型,但我们需要从 app.collections.todos 集合创建一个实例。
// app.jsinit: function() { this.content = $(#content); this.todos = new api.collections.todos(); return this;}
显示我们的第一个视图(主导航)我们必须展示的第一件事是主应用程序的导航。
// views/menu.jsapp.views.menu = backbone.view.extend({ template: _.template($(#tpl-menu).html()), initialize: function() { this.render(); }, render: function(){ this.$el.html(this.template({})); }});
虽然只有九行代码,但这里发生了很多很酷的事情。第一个是设置模板。如果您还记得,我们将 underscore.js 添加到了我们的应用程序中?我们将使用它的模板引擎,因为它运行良好并且使用起来很简单。
_.template(templatestring, [data], [settings])
最后有一个函数,它接受一个以键值对形式保存信息的对象,而 templatestring 是 html 标记。好的,它接受一个 html 字符串,但是 $(#tpl-menu).html() 在那里做什么?当我们开发小型单页面应用程序时,我们通常将模板直接放入页面中,如下所示:
// index.html<script type=text/template id=tpl-menu> <ul> <li><a href=#>list</a></li> <li><a href=#archive>archive</a></li> <li class=right><a href=#new>+</a></li> </ul></script>
由于它是一个脚本标记,因此不会向用户显示。从另一个角度来看,它是一个有效的 dom 节点,因此我们可以使用 jquery 获取其内容。因此,上面的简短片段仅获取该脚本标记的内容。
render 方法在 backbone.js 中非常重要。这就是显示数据的函数。通常,您将模型触发的事件直接绑定到该方法。然而,对于主菜单,我们不需要这样的行为。
this.$el.html(this.template({}));
this.$el 是框架创建的一个对象,每个视图默认都有它(el 前面有一个 $ 因为我们包含了 jquery)。默认情况下,它是一个空的 。当然,您可以使用 tagname 属性来更改它。但这里更重要的是,我们没有直接为该对象赋值。我们不会改变它,我们只是改变它的内容。上面一行和下一行有很大的区别:
this.$el = $(this.template({}));
重点是,如果你想在浏览器中看到变化,你应该先调用 render 方法,将视图附加到 dom 中。否则只会附加空的 div。还有另一种情况,您有嵌套视图。而且由于您直接更改属性,因此父组件不会更新。绑定的事件也可能被破坏,您需要重新附加侦听器。因此,您实际上应该只更改 this.$el 的内容,而不是属性的值。
视图现已准备就绪,我们需要初始化它。让我们将其添加到我们的工厂模块中:
// app.jsvar viewsfactory = { menu: function() { if(!this.menuview) { this.menuview = new api.views.menu({ el: $(#menu) }); } return this.menuview; }};
最后只需调用引导区域中的 menu 方法即可:
// app.jsinit: function() { this.content = $(#content); this.todos = new api.collections.todos(); viewsfactory.menu(); return this;}
请注意,当我们从导航类创建新实例时,我们传递了一个已经存在的 dom 元素 $(#menu)。所以,视图中的 this.$el 属性实际上指向 $(#menu)。
添加路线backbone.js 支持推送状态操作。换句话说,您可以操纵当前浏览器的 url 并在页面之间移动。但是,我们将坚持使用旧的哈希类型 url,例如 /#edit/3。
// app.jsvar router = backbone.router.extend({ routes: { archive: archive, new: newtodo, edit/:index: edittodo, delete/:index: deltetodo, : list }, list: function(archive) {}, archive: function() {}, newtodo: function() {}, edittodo: function(index) {}, deltetodo: function(index) {}});
上面是我们的路由器。哈希对象中定义了五个路由。键是您将在浏览器地址栏中键入的内容,值是将要调用的函数。请注意,其中两条路由上有 :index。如果您想支持动态 url,则需要使用该语法。在我们的例子中,如果您输入 #edit/3 ,则 edittodo 将使用参数 index=3 执行。最后一行包含一个空字符串,这意味着它处理我们应用程序的主页。
显示所有任务的列表到目前为止,我们构建的是我们项目的主视图。它将从集合中检索数据并将其打印在屏幕上。我们可以将相同的视图用于两件事 - 显示所有活动的待办事项和显示已存档的待办事项。
在继续列表视图实现之前,让我们看看它是如何实际初始化的。
// in app.js views factorylist: function() { if(!this.listview) { this.listview = new api.views.list({ model: api.todos }); } return this.listview;}
请注意,我们正在传递集合。这很重要,因为我们稍后将使用 this.model 来访问存储的数据。工厂返回我们的列表视图,但路由器是必须将其添加到页面的人。
// in app.js's routerlist: function(archive) { var view = viewsfactory.list(); api .title(archive ? archive: : your todos:) .changecontent(view.$el); view.setmode(archive ? archive : null).render();}
目前,在路由器中调用方法 list ,不带任何参数。因此该视图不是 archive 模式,它只会显示活动的 todos。
// views/list.jsapp.views.list = backbone.view.extend({ mode: null, events: {}, initialize: function() { var handler = _.bind(this.render, this); this.model.bind('change', handler); this.model.bind('add', handler); this.model.bind('remove', handler); }, render: function() {}, priorityup: function(e) {}, prioritydown: function(e) {}, archive: function(e) {}, changestatus: function(e) {}, setmode: function(mode) { this.mode = mode; return this; }});
渲染期间将使用 mode 属性。如果其值为 mode=archive 则仅显示已存档的 todos。 events 是一个我们将立即填充的对象。这是我们放置 dom 事件映射的地方。其余方法是用户交互的响应,它们直接链接到所需的功能。例如, priorityup 和 prioritydown 更改待办事项的顺序。 archive 将项目移动到存档区域。 changestatus 只是将 todo 标记为已完成。
initialize 方法内部发生的事情很有趣。前面我们说过,通常您会将模型(在我们的例子中为集合)中的更改绑定到视图的 render 方法。您可以输入 this.model.bind('change', this.render)。但很快您就会注意到 this 关键字在 render 方法中不会指向视图本身。这是因为范围发生了变化。作为解决方法,我们正在创建一个具有已定义范围的处理程序。这就是 underscore 的 bind 函数的用途。
这里是 render 方法的实现。
// views/list.jsrender: function() {) var html = '<ul class=list>', self = this; this.model.each(function(todo, index) { if(self.mode === archive ? todo.get(archived) === true : todo.get(archived) === false) { var template = _.template($(#tpl-list-item).html()); html += template({ title: todo.get(title), index: index, archivelink: self.mode === archive ? unarchive : archive, done: todo.get(done) ? yes : no, donechecked: todo.get(done) ? 'checked==checked' : }); } }); html += '</ul>'; this.$el.html(html); this.delegateevents(); return this;}
我们循环遍历集合中的所有模型并生成一个 html 字符串,稍后将其插入到视图的 dom 元素中。很少有检查可以区分待办事项从已存档到活动。在复选框的帮助下,任务被标记为完成。因此,为了表明这一点,我们需要将 checked==checked 属性传递给该元素。您可能会注意到我们正在使用 this.delegateevents()。在我们的例子中这是必要的,因为我们正在从 dom 中分离和附加视图。是的,我们不会替换主元素,但事件的处理程序将被删除。这就是为什么我们必须告诉 backbone.js 再次附加它们。上面代码中使用的模板是:
// index.html<script type=text/template id=tpl-list-item> <li class=cf done-<%= done %> data-index=<%= index %>> <h2> <input type=checkbox data-status <%= donechecked %> /> <a href=javascript:void(0); data-up>↑</a> <a href=javascript:void(0); data-down>↓</a> <%= title %> </h2> <div class=options> <a href=#edit/<%= index %>>edit</a> <a href=javascript:void(0); data-archive><%= archivelink %></a> <a href=#delete/<%= index %>>delete</a> </div> </li></script>
请注意,定义了一个名为 done-yes 的 css 类,它将 todo 绘制为绿色背景。除此之外,还有很多链接,我们将使用它们来实现所需的功能。它们都具有数据属性。元素的主节点li,有data-index。该属性的值显示任务在集合中的索引。请注意,包裹在 中的特殊表达式被发送到 template 函数。这就是注入到模板中的数据。
是时候向视图添加一些事件了。
// views/list.jsevents: { 'click a[data-up]': 'priorityup', 'click a[data-down]': 'prioritydown', 'click a[data-archive]': 'archive', 'click input[data-status]': 'changestatus'}
在 backbone.js 中,事件的定义只是一个哈希值。您首先输入事件的名称,然后输入选择器。属性的值实际上是视图的方法。
// views/list.jspriorityup: function(e) { var index = parseint(e.target.parentnode.parentnode.getattribute(data-index)); this.model.up(index);},prioritydown: function(e) { var index = parseint(e.target.parentnode.parentnode.getattribute(data-index)); this.model.down(index);},archive: function(e) { var index = parseint(e.target.parentnode.parentnode.getattribute(data-index)); this.model.archive(this.mode !== archive, index); },changestatus: function(e) { var index = parseint(e.target.parentnode.parentnode.getattribute(data-index)); this.model.changestatus(e.target.checked, index); }
这里我们使用 e.target 进入处理程序。它指向触发事件的 dom 元素。我们正在获取单击的 todo 的索引并更新集合中的模型。通过这四个函数,我们完成了我们的课程,现在数据显示在页面上。
正如我们上面提到的,我们将为 archive 页面使用相同的视图。
list: function(archive) { var view = viewsfactory.list(); api .title(archive ? archive: : your todos:) .changecontent(view.$el); view.setmode(archive ? archive : null).render();},archive: function() { this.list(true);}
上面是与之前相同的路由处理程序,但这次使用 true 作为参数。
添加和编辑待办事项按照列表视图的入门,我们可以创建另一个显示用于添加和编辑任务的表单的列表视图。下面是这个新类的创建方式:
// app.js / views factoryform: function() { if(!this.formview) { this.formview = new api.views.form({ model: api.todos }).on(saved, function() { api.router.navigate(, {trigger: true}); }) } return this.formview;}
几乎一样。然而,这次表单提交后我们需要做一些事情。这会将用户转发到主页。正如我所说,每个扩展 backbone.js 类的对象实际上都是一个事件调度程序。您可以使用 on 和 trigger 等方法。
在继续查看代码之前,让我们看一下 html 模板:
<script type=text/template id=tpl-form> <form> <textarea><%= title %></textarea> <button>save</button> </form></script>
我们有一个 textarea 和一个 button。如果我们要添加新任务,该模板需要一个 title 参数,该参数应该是一个空字符串。
// views/form.jsapp.views.form = backbone.view.extend({ index: false, events: { 'click button': 'save' }, initialize: function() { this.render(); }, render: function(index) { var template, html = $(#tpl-form).html(); if(typeof index == 'undefined') { this.index = false; template = _.template(html, { title: }); } else { this.index = parseint(index); this.todoforediting = this.model.at(this.index); template = _.template($(#tpl-form).html(), { title: this.todoforediting.get(title) }); } this.$el.html(template); this.$el.find(textarea).focus(); this.delegateevents(); return this; }, save: function(e) { e.preventdefault(); var title = this.$el.find(textarea).val(); if(title == ) { alert(empty textarea!); return; } if(this.index !== false) { this.todoforediting.set(title, title); } else { this.model.add({ title: title }); } this.trigger(saved); }});
该视图只有 40 行代码,但它的工作效果很好。仅附加一个事件,即单击“保存”按钮。根据传递的 index 参数,渲染方法的行为有所不同。例如,如果我们正在编辑 todo,我们会传递索引并获取确切的模型。如果没有,则表单为空,并且将创建一个新任务。上面的代码中有几个有趣的点。首先,在渲染中,我们使用 .focus() 方法在渲染视图后将焦点带到表单上。应再次调用 delegateevents 函数,因为表单可以分离并再次附加。 save 方法以 e.preventdefault() 开头。这会删除按钮的默认行为,在某些情况下可能会提交表单。最后,一旦一切完成,我们就会触发 saved 事件,通知外界 todo 已保存到集合中。
路由器有两种方法需要填写。
// app.jsnewtodo: function() { var view = viewsfactory.form(); api.title(create new todo:).changecontent(view.$el); view.render()},edittodo: function(index) { var view = viewsfactory.form(); api.title(edit:).changecontent(view.$el); view.render(index);}
它们之间的区别在于,我们传入一个索引,如果 edit/:index 路由匹配。当然,页面标题也会相应更改。
从集合中删除记录对于此功能,我们不需要视图。整个工作可以直接在路由器的处理程序中完成。
deltetodo: function(index) { api.todos.remove(api.todos.at(parseint(index))); api.router.navigate(, {trigger: true});}
我们知道要删除的 todo 的索引。集合类中有一个 remove 方法,它接受模型对象。最后,只需将用户转发到主页,其中就会显示更新后的列表。
结论backbone.js 拥有构建功能齐全的单页应用程序所需的一切。我们甚至可以将其绑定到 rest 后端服务,框架将同步您的应用程序和数据库之间的数据。事件驱动方法鼓励模块化编程以及良好的架构。我个人在多个项目中使用 backbone.js,并且效果非常好。
以上就是backbone.js 为单页 todo 应用程序提供支持的详细内容。
