backbone官方Todo示例分解学习

本文介绍了一个基于Backbone.js框架实现的待办事项应用,详细解释了模型、集合、视图之间的交互原理及事件驱动流程。文章通过具体代码示例展示了如何使用Backbone.Model、Backbone.Collection和Backbone.View构建复杂应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

配合浏览器调试学习效果更佳,ps:嫌弃复制黏贴的,移步至点击打开链接下载。


1.js部分(todos.js)

$(function () {
    /**
     * 执行顺序为
     * AppView 获取了id为"todo-list"的ul
     * ->然后是TodoView创建了一个tagName li元素,并将#item-template作为需要编译的模板插入这个创建的li元素中,所以此时的
     * TodoView对象中的this.$()指针只能获取到li元素下的元素
     * 再然后就是AppView监听模型的集合todos,todos包括todo模型,通过事件的改变,触发一系列的响应函数
     */
    var Todo = Backbone.Model.extend({
        //defaults,默认属性,也就是model.get();方法能取到的值
        defaults: function () {
            return {
                title: "empty todo...",
                order: Todos.nextOrder(),
                done: false
            };
        },
        //模型自定义的toggle方法,请区别Jq的toggle方法
        toggle: function () {
            this.save({done: !this.get("done")});
        }

    });

    var TodoList = Backbone.Collection.extend({
        model: Todo,//Todo模型的集合,视图产生的模型都集合在这里
        localStorage: new Backbone.LocalStorage("todos-backbone"),
        done: function () {
            return this.where({done: true});
        },
        remaining: function () {
            return this.where({done: false});
        },
        nextOrder: function () {
            console.log("第三步,新模型创建之后利用传入的参数{title: this.input.val()}初始化模型,初始化order的时候调用了此处的nextOrder函数,");
            console.log("nextOrder this" + JSON.stringify(this));
            console.log("nextOrder this.length" + this.length);
            if (!this.length) return 1;
            return this.last().get('order') + 1;
        },
        comparator: 'order'

    });
    var Todos = new TodoList;

    var TodoView = Backbone.View.extend({
        tagName: "li",//tagName id className都是用于创建一个新元素,如果tagName不指定值的话,默认为div,而属性el:'#selector'才是直接绑定现有的selector
        template: _.template($('#item-template').html()),
        events: {//此处功能为为dom对象绑定事件,并绑定事件触发后的执行函数
            "click .toggle": "toggleDone",
            /**
             * 因为是事件驱动,所以就从这里开始讲诉这个驱动的流程
             * 此处为一个类为toggle的元素绑定了click事件,触发的执行函数为toggleDone,
             * toggleDone调用模型的toggle方法,模型的toggle方法调用underscore的save方法,
             * save方法改变了模型属性之后,就会触发change事件,
             * 而change事件在new 一个视图(具体为TodoView视图)时所自动执行的initialize方法中被监听,
             * 并触发该事件所绑定的render方法,
             * 而在render方法中,underscore将预先定义的模板编译成html语言,插入到事先定义好的tagName li 中
             * 这样就完成了一个流程
             */
            "dblclick .view": "edit",
            "click a.destroy": "clear",
            "keypress .edit": "updateOnEnter",
            "blur .edit": "close"
        },
        initialize: function () {//此处功能为监听模型的change事件,监听到之后执行render方法(具体为刷新视图,当然在刷新视图的同时也可以做一些其他的事)
            this.listenTo(this.model, 'change', this.render);
            //千注意万注意,此处this.model并不是直接指向Todo,而是指向collection集合中的模型,至于指向的是哪个模型,则由传入的参数决定,
            //具体例子请看AppView里面的addOne方法,该方法{model:todo},
            // 意思就是我的TodoView视图中所指向的model为传入的todo模型(具体为createOnEnter方法中传入的{title: this.input.val()}模型参数)
            this.listenTo(this.model, 'destroy', this.remove);
        },
        render: function () {//此处功能为更新视图
            console.log("第五步,在append()方法中顺便调用了TodoView视图的render方法,该方法将模板编译为可执行html并插入模板中,返回的this指针指向li元素,便完成了一次添加以及视图的更新,然后此时因为render方法被执行了两次,所以你会看到这句话被输出了两次");
            console.log("(*********************Todos:" + Todos);
            console.log("(*********************stringify Todos:" + JSON.stringify(Todos));
            console.log("(*********************Todos.length:" + Todos.length);
            /**
             * {title: "you is cute", order: 2, done: false, id: "51041ecc-19f2-5a75-e369-8600d290febd"}
             */
            console.log(this.model.toJSON());
            this.$el.html(this.template(this.model.toJSON()));//渲染模板,将todo模型数据通过template编译然后呈现在index页面,主要是title以及根据done状态判断是否checked
            this.$el.toggleClass('done', this.model.get('done'));//添加css done类,实现效果为单击时改变字体颜色并且文字加上中划线表示此项已经完成
            this.input = this.$('.edit');
            console.log('***xq debug***:this.$: 取不到:' + this.$('.xq label').text());//这是取不到的,因为this.$()等价于$(view.el).find(selector),也就是要在el元素的子元素中查找
            console.log('***xq debug***:$: 取得到:' + $('.xq label').text());//这就能取到
            return this;
        },
        toggleDone: function () {
            this.model.toggle();//调用todo模型的toggle方法,this.save({done: !this.get("done")});更新done的状态
        },
        edit: function () {
            this.$el.addClass("editing");
            this.input.focus();
        },
        close: function () {
            var value = this.input.val();
            if (!value) {
                this.clear();
            } else {
                this.model.save({title: value});
                this.$el.removeClass("editing");
            }
        },
        updateOnEnter: function (e) {//双击编辑已经保存的边条的时候响应,enter(keyCode == 13)时保存编辑的内容
            console.log("************e.keyCode" + e.keyCode);
            if (e.keyCode == 13) this.close();
        },
        clear: function () {//失去焦点的时候input输入框隐藏
            this.model.destroy();
        }

    });

    //我们高于一切的AppView是UI的最高等级部分
    var AppView = Backbone.View.extend({
        //区别于新增一个目标元素,我们使用现有元素
        el: $("#todoapp"),
        statsTemplate: _.template($('#stats-template').html()),
        events: {
            "keypress #new-todo": "createOnEnter",
            "click #clear-completed": "clearCompleted",
            "click #toggle-all": "toggleAllComplete"
        },
        initialize: function () {

            this.input = this.$("#new-todo");
            this.allCheckbox = this.$("#toggle-all")[0];

            this.listenTo(Todos, 'add', this.addOne);//监听Todos集合的添加事件
            this.listenTo(Todos, 'reset', this.addAll);
            this.listenTo(Todos, 'all', this.render);//监听所有集合事件

            this.footer = this.$('footer');
            this.main = $('#main');//原生JQuery也是能用的

            Todos.fetch();
        },
        render: function () {
            var done = Todos.done().length;
            var remaining = Todos.remaining().length;

            if (Todos.length) {
                this.main.show();
                this.footer.show();
                this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
            } else {
                this.main.hide();
                this.footer.hide();
            }

            this.allCheckbox.checked = !remaining;
        },
        addOne: function (todo) {
            console.log("第四步,集合中添加了新模型,触发了Todos集合的add事件,执行了addOne方法。");
            console.log("*************addOne todo" + JSON.stringify(todo));
            var view = new TodoView({model: todo});
            console.log("然后addOne方法中new了一个TodoView视图,并将该新new的视图所对应的模型指向随参传入的模型{title: this.input.val()}。(new该视图的时候便执行了initialize方法,该方法监听了该视图对应的模型的变化,变化时便执行render方法,将视图指向新的模型也属于模型变化,所以此处TodoView视图的render方法就被执行了一次,主要是渲染模板)");
            this.$("#todo-list").append(view.render().el);//view.render()初始化模板并返回this指针,this.el='li',就变成了append('li')
        },
        addAll: function () {
            Todos.each(this.addOne, this);//为集合中的每个模型添加addOne事件
        },
        createOnEnter: function (e) {
            console.log("第一步,当输入完毕单击enter时候createOnEnter函数被触发。");
            if (e.keyCode != 13) return;
            if (!this.input.val()) return;
            console.log("第二步,createOnEnter方法中Todos集合create了一个新模型");
            Todos.create({title: this.input.val()});//model:Todo
            //此时的流程为:
            /**
             * 在编辑框按下enter键时候就新添加一个模型进Todos集合里面,create方法,带参数为{title: this.input.val()}
             * 集合里面添加了模型,则触发Todos模型的add事件,
             * 而add事件在AppView视图的initialize方法中被绑定了addOne方法,
             * 而addOne方法new了一个TodoView视图,并传入参数为{title: this.input.val()},
             * 并且在想执行了TodoView视图的render()方法之后(更新模板),再往ul元素添加更新了的li模板
             */
            this.input.val('');
        },
        clearCompleted: function () {
            _.invoke(Todos.done(), 'destroy');
            return false;
        },
        toggleAllComplete: function () {
            var done = this.allCheckbox.checked;
            Todos.each(function (todo) {
                todo.save({'done': done});
            });
        }

    });
    //最后,我们通过新建一个App来开始我们的程序
    var App = new AppView;

});

2.html页面(index.html)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Backbone.js Todos</title>
    <link rel="stylesheet" href="todos.css"/>
</head>

<body>

<div id="todoapp">

    <header>
        <h1>Todos</h1>
        <input id="new-todo" type="text" placeholder="What needs to be done?">
    </header>

    <section id="main">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list"></ul>
    </section>

    <footer>
        <a id="clear-completed">Clear completed</a>

        <div id="todo-count"></div>
    </footer>

</div>

<div id="instructions">
    Double-click to edit a todo.
</div>

<div id="credits">
    Created by
    <br/>
    <a href="http://jgn.me/">Jérôme Gravel-Niquet</a>.
    <br/>Rewritten by: <a href="https://github.com/tastejs/todomvc">TodoMVC</a>.
</div>
<div class="xq">
    <label>here is xq</label>
</div>

<script src="../../test/vendor/json2.js"></script>
<script src="../../test/vendor/jquery.js"></script>
<script src="../../test/vendor/underscore.js"></script>
<script src="../../backbone.js"></script>
<script src="../backbone.localStorage.js"></script>
<script src="todos.js"></script>

<!-- Templates -->

<script type="text/template" id="item-template">
    <div class="view">
        <input class="toggle" type="checkbox" <%= done ? 'checked="checked"' : '' %> />
        <label><%- title %></label>
        <a class="destroy"></a>
    </div>
    <input class="edit" type="text" value="<%- title %>"/>
</script>

<script type="text/template" id="stats-template">
    <% if (done) { %>
    <a id="clear-completed">Clear <%= done %> completed <%= done == 1 ? 'item' : 'items' %></a>
    <% } %>
    <div class="todo-count"><b><%= remaining %></b> <%= remaining == 1 ? 'item' : 'items' %> left</div>
</script>

</body>
</html>
3.css部分(todos.css)

html,
body {
	margin: 0;
	padding: 0;
}

body {
	font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #eeeeee;
	color: #333333;
	width: 520px;
	margin: 0 auto;
	-webkit-font-smoothing: antialiased;
}

#todoapp {
	background: #fff;
	padding: 20px;
	margin-bottom: 40px;
	-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
	-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
	-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
	-o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
	box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
	-webkit-border-radius: 0 0 5px 5px;
	-moz-border-radius: 0 0 5px 5px;
	-ms-border-radius: 0 0 5px 5px;
	-o-border-radius: 0 0 5px 5px;
	border-radius: 0 0 5px 5px;
}

#todoapp h1 {
	font-size: 36px;
	font-weight: bold;
	text-align: center;
	padding: 0 0 10px 0;
}

#todoapp input[type="text"] {
	width: 466px;
	font-size: 24px;
	font-family: inherit;
	line-height: 1.4em;
	border: 0;
	outline: none;
	padding: 6px;
	border: 1px solid #999999;
	-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
	-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
	-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
	-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
	box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}

#todoapp input::-webkit-input-placeholder {
	font-style: italic;
}

#main {
	display: none;
}

#todo-list {
	margin: 10px 0;
	padding: 0;
	list-style: none;
}

#todo-list li {
	padding: 18px 20px 18px 0;
	position: relative;
	font-size: 24px;
	border-bottom: 1px solid #cccccc;
}

#todo-list li:last-child {
	border-bottom: none;
}

#todo-list li.done label {
	color: #777777;
	text-decoration: line-through;
}

#todo-list .destroy {
	position: absolute;
	right: 5px;
	top: 20px;
	display: none;
	cursor: pointer;
	width: 20px;
	height: 20px;
	background: url(destroy.png) no-repeat;
}

#todo-list li:hover .destroy {
  	display: block;
}

#todo-list .destroy:hover {
  	background-position: 0 -20px;
}

#todo-list li.editing {
	border-bottom: none;
	margin-top: -1px;
	padding: 0;
}

#todo-list li.editing:last-child {
	margin-bottom: -1px;
}

#todo-list li.editing .edit {
	display: block;
	width: 444px;
	padding: 13px 15px 14px 20px;
	margin: 0;
}

#todo-list li.editing .view {
	display: none;
}

#todo-list li .view label {
	word-break: break-word;
}

#todo-list li .edit {
	display: none;
}

#todoapp footer {
	display: none;
	margin: 0 -20px -20px -20px;
	overflow: hidden;
	color: #555555;
	background: #f4fce8;
	border-top: 1px solid #ededed;
	padding: 0 20px;
	line-height: 37px;
	-webkit-border-radius: 0 0 5px 5px;
	-moz-border-radius: 0 0 5px 5px;
	-ms-border-radius: 0 0 5px 5px;
	-o-border-radius: 0 0 5px 5px;
	border-radius: 0 0 5px 5px;
}

#clear-completed {
	float: right;
	line-height: 20px;
	text-decoration: none;
	background: rgba(0, 0, 0, 0.1);
	color: #555555;
	font-size: 11px;
	margin-top: 8px;
	margin-bottom: 8px;
	padding: 0 10px 1px;
	cursor: pointer;
	-webkit-border-radius: 12px;
	-moz-border-radius: 12px;
	-ms-border-radius: 12px;
	-o-border-radius: 12px;
	border-radius: 12px;
	-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
	-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
	-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
	-o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
	box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
}

#clear-completed:hover {
	background: rgba(0, 0, 0, 0.15);
	-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
	-moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
	-ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
	-o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
	box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
}

#clear-completed:active {
	position: relative;
	top: 1px;
}

#todo-count span {
	font-weight: bold;
}

#instructions {
	margin: 10px auto;
	color: #777777;
	text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
	text-align: center;
}

#instructions a {
	color: #336699;
}

#credits {
	margin: 30px auto;
	color: #999;
	text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
	text-align: center;
}

#credits a {
	color: #888;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值