首頁 >web前端 >js教程 >Backbone.js 為單頁 ToDo 應用程式提供支持

Backbone.js 為單頁 ToDo 應用程式提供支持

王林
王林原創
2023-08-29 15:17:081104瀏覽

Backbone.js 为单页 ToDo 应用程序提供支持

Backbone.js 是一個用於建立靈活的 Web 應用程式的 JavaScript 框架。它帶有模型、集合、視圖、事件、路由器和其他一些很棒的功能。在本文中,我們將開發一個簡單的 ToDo 應用程序,它支援新增、編輯和刪除任務。我們還應該能夠將任務標記為“已完成”並將其存檔。為了保持這篇文章的長度合理,我們不會包含任何與資料庫的通訊。所有資料都將保存在客戶端。

#

設定

這是我們將要使用的檔案結構:

css
    └── styles.css
js
    └── 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.js
var 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 變數是傳回的對象,代表類別的公共方法。 viewsmodelscollections 屬性將充當 Backbone.js 傳回的類別的持有者。 content 是一個指向主使用者介面容器的 jQuery 元素。這裡有兩個輔助方法。第一個更新該容器。第二個設定頁面的標題。然後我們定義了一個名為 ViewsFactory 的模組。它將傳遞我們的視圖,最後,我們建立了路由器。

你可能會問,為什麼我們需要一個工廠來儲存視圖?嗯,使用 Backbone.js 時有一些常見的模式。其中之一與視圖的建立和使用有關。

var ViewClass = Backbone.View.extend({ /* logic here */ });
var view = new ViewClass();

最好只初始化視圖一次並讓它們保持活動狀態。一旦資料發生更改,我們通常會呼叫視圖的方法並更新其 el 物件的內容。另一種非常流行的方法是重新建立整個視圖或取代整個 DOM 元素。然而,從性能的角度來看,這並不是很好。因此,我們通常會得到一個實用程式類,它會建立視圖的一個實例並在需要時傳回它。

元件定義

我們有了一個命名空間,所以現在我們可以開始建立元件了。主選單如下所示:

// views/menu.js
app.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.js
app.models.ToDo = Backbone.Model.extend({
    defaults: {
        title: "ToDo",
        archived: false,
        done: false
    }
});

只有三個欄位。第一個包含任務文本,另外兩個是定義記錄狀態的標誌。

框架内的所有东西实际上都是一个事件调度程序。由于模型是通过设置器更改的,因此框架知道数据何时更新,并可以通知系统的其余部分。一旦您将某些内容绑定到这些通知,您的应用程序就会对模型中的更改做出反应。这是 Backbone.js 中一个非常强大的功能。

正如我一开始所说的,我们将有很多记录,我们将它们组织到一个名为 ToDos 的集合中。

// collections/ToDos.js
app.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 属性。它告诉类正在存储什么类型的数据。其余方法实现与我们应用程序中的功能相关的自定义逻辑。 updown 函数更改 ToDos 的顺序。为了简化事情,我们将仅使用集合数组中的索引来标识每个 ToDo。这意味着如果我们想要获取一条特定记录,我们应该指向它的索引。所以,排序只是交换数组中的元素。正如您可能从上面的代码中猜到的那样, this.models 是我们正在讨论的数组。 archivechangeStatus 设置给定元素的属性。我们将这些方法放在这里,因为视图将有权访问 ToDos 集合,而不是直接访问任务。

此外,我们不需要从 app.models.ToDo 类创建任何模型,但我们需要从 app.collections.ToDos 集合创建一个实例。

// App.js
init: function() {
    this.content = $("#content");
    this.todos = new api.collections.ToDos();
    return this;
}

显示我们的第一个视图(主导航)

我们必须展示的第一件事是主应用程序的导航。

// views/menu.js
app.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.js
var ViewsFactory = {
    menu: function() {
        if(!this.menuView) {
            this.menuView = new api.views.menu({ 
                el: $("#menu")
            });
        }
        return this.menuView;
    }
};

最后只需调用引导区域中的 menu 方法即可:

// App.js
init: 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.js
var 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 factory
list: function() {
    if(!this.listView) {
        this.listView = new api.views.list({
            model: api.todos
        });
    }   
    return this.listView;
}

请注意,我们正在传递集合。这很重要,因为我们稍后将使用 this.model 来访问存储的数据。工厂返回我们的列表视图,但路由器是必须将其添加到页面的人。

// in App.js's router
list: 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.js
app.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 事件映射的地方。其余方法是用户交互的响应,它们直接链接到所需的功能。例如, priorityUppriorityDown 更改待办事项的顺序。 archive 将项目移动到存档区域。 changeStatus 只是将 ToDo 标记为已完成。

initialize 方法内部发生的事情很有趣。前面我们说过,通常您会将模型(在我们的例子中为集合)中的更改绑定到视图的 render 方法。您可以输入 this.model.bind('change', this.render)。但很快您就会注意到 this 关键字在 render 方法中不会指向视图本身。这是因为范围发生了变化。作为解决方法,我们正在创建一个具有已定义范围的处理程序。这就是 Underscore 的 bind 函数的用途。

这里是 render 方法的实现。

// views/list.js
render: 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>&#8593;</a>
            <a href="javascript:void(0);" data-down>&#8595;</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.js
events: {
    'click a[data-up]': 'priorityUp',
    'click a[data-down]': 'priorityDown',
    'click a[data-archive]': 'archive',
    'click input[data-status]': 'changeStatus'
}

在 Backbone.js 中,事件的定义只是一个哈希值。您首先输入事件的名称,然后输入选择器。属性的值实际上是视图的方法。

// views/list.js
priorityUp: 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 factory
form: 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 类的对象实际上都是一个事件调度程序。您可以使用 ontrigger 等方法。

在继续查看代码之前,让我们看一下 HTML 模板:

<script type="text/template" id="tpl-form">
    <form>
        <textarea><%= title %></textarea>
        <button>save</button>
    </form>
</script>

我们有一个 textarea 和一个 button。如果我们要添加新任务,该模板需要一个 title 参数,该参数应该是一个空字符串。

// views/form.js
app.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.js
newToDo: 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 應用程式提供支持的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn