以todomvc爲例分析knockout、backbone和angularjs

1、總體結構

項目github地址https://github.com/tastejs/todomvc/ css

排除通用的css樣式文件和引用的js庫文件,僅看html和jshtml

1.1 knockoutjs版todo app文件結構

knockoutjs
--index.html
--js
----app.js 

1.2 backbonejs版todo app文件結構

backbonejs
--index.html
--js
----collections
------todos.js
----models
------todo.js
----routers
------router.js
----views
------app-view.js
------todo-view.js
----app.js 

1.3 angularjs版todo app文件結構

angularjs
--index.html
--js
----controllers
------todoCtrl.js
----directives
------todoEscape.js
----services
------todoStorage.js
----app.js

2、knockout版todo主要內容

 knockout版todo app實現細節,以前有文講過,詳情見《用KnockoutJS實現ToDoMVC代碼分析》git

從上文的文件結構可知,其業務代碼只有app.js,html view只有index.htmlangularjs

2.1 視圖代碼index.html

knockout在html原有屬性基礎上,新增了data-bind屬性github

data-bind屬性做爲knockout與html交互的入口,內置了以下若干指令express

  • visible binding
  • text binding
  • html binding
  • css binding
  • style binding
  • attr binding

除了上述內置指令,knockout也能夠添加自定義指令,如html中出現的enterKeyescapeKey和selectAndFocus指令segmentfault

<section id="todoapp">
            <header id="header">
                <h1>todos</h1>
                <input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" autofocus>
            </header>
            <section id="main" data-bind="visible: todos().length">
                <input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
                <label for="toggle-all">Mark all as complete</label>
                <ul id="todo-list" data-bind="foreach: filteredTodos">
                    <li data-bind="css: { completed: completed, editing: editing }">
                        <div class="view">
                            <input class="toggle" data-bind="checked: completed" type="checkbox">
                            <label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
                            <button class="destroy" data-bind="click: $root.remove"></button>
                        </div>
                        <input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }">
                    </li>
                </ul>
            </section>
            <footer id="footer" data-bind="visible: completedCount() || remainingCount()">
                <span id="todo-count">
                    <strong data-bind="text: remainingCount">0</strong>
                    <span data-bind="text: getLabel(remainingCount)"></span> left
                </span>
                <ul id="filters">
                    <li>
                        <a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
                    </li>
                    <li>
                        <a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
                    </li>
                    <li>
                        <a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
                    </li>
                </ul>
                <button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">Clear completed</button>
            </footer>
        </section>
View Code

2.2 業務代碼app.js

 app.js中,首先對html view中自定義的指令enterKey、escapeKey和selectAndFocus作了定義promise

var ENTER_KEY = 13;
    var ESCAPE_KEY = 27;

    // A factory function we can use to create binding handlers for specific
    // keycodes.
    function keyhandlerBindingFactory(keyCode) {
        return {
            init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
                var wrappedHandler, newValueAccessor;

                // wrap the handler with a check for the enter key
                wrappedHandler = function (data, event) {
                    if (event.keyCode === keyCode) {
                        valueAccessor().call(this, data, event);
                    }
                };

                // create a valueAccessor with the options that we would want to pass to the event binding
                newValueAccessor = function () {
                    return {
                        keyup: wrappedHandler
                    };
                };

                // call the real event binding's init function
                ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, data, bindingContext);
            }
        };
    }

    // a custom binding to handle the enter key
    ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY);

    // another custom binding, this time to handle the escape key
    ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY);

    // wrapper to hasFocus that also selects text and applies focus async
    ko.bindingHandlers.selectAndFocus = {
        init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
            ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
            ko.utils.registerEventHandler(element, 'focus', function () {
                element.focus();
            });
        },
        update: function (element, valueAccessor) {
            ko.utils.unwrapObservable(valueAccessor()); // for dependency
            // ensure that element is visible before trying to focus
            setTimeout(function () {
                ko.bindingHandlers.hasFocus.update(element, valueAccessor);
            }, 0);
        }
    };
View Code

而後定義了todo modelmvc

// represent a single todo item
    var Todo = function (title, completed) {
        this.title = ko.observable(title);
        this.completed = ko.observable(completed);
        this.editing = ko.observable(false);
    };

ViewModel中定義了html view中的業務方法和屬性app

// our main view model
    var ViewModel = function (todos) {
        // map array of passed in todos to an observableArray of Todo objects
        this.todos = ko.observableArray(todos.map(function (todo) {
            return new Todo(todo.title, todo.completed);
        }));

        // store the new todo value being entered
        this.current = ko.observable();

        this.showMode = ko.observable('all');

        this.filteredTodos = ko.computed(function () {
            switch (this.showMode()) {
            case 'active':
                return this.todos().filter(function (todo) {
                    return !todo.completed();
                });
            case 'completed':
                return this.todos().filter(function (todo) {
                    return todo.completed();
                });
            default:
                return this.todos();
            }
        }.bind(this));

        // add a new todo, when enter key is pressed
        this.add = function () {
            var current = this.current().trim();
            if (current) {
                this.todos.push(new Todo(current));
                this.current('');
            }
        }.bind(this);

        // remove a single todo
        this.remove = function (todo) {
            this.todos.remove(todo);
        }.bind(this);

        // remove all completed todos
        this.removeCompleted = function () {
            this.todos.remove(function (todo) {
                return todo.completed();
            });
        }.bind(this);

        // edit an item
        this.editItem = function (item) {
            item.editing(true);
            item.previousTitle = item.title();
        }.bind(this);

        // stop editing an item.  Remove the item, if it is now empty
        this.saveEditing = function (item) {
            item.editing(false);

            var title = item.title();
            var trimmedTitle = title.trim();

            // Observable value changes are not triggered if they're consisting of whitespaces only
            // Therefore we've to compare untrimmed version with a trimmed one to chech whether anything changed
            // And if yes, we've to set the new value manually
            if (title !== trimmedTitle) {
                item.title(trimmedTitle);
            }

            if (!trimmedTitle) {
                this.remove(item);
            }
        }.bind(this);

        // cancel editing an item and revert to the previous content
        this.cancelEditing = function (item) {
            item.editing(false);
            item.title(item.previousTitle);
        }.bind(this);

        // count of all completed todos
        this.completedCount = ko.computed(function () {
            return this.todos().filter(function (todo) {
                return todo.completed();
            }).length;
        }.bind(this));

        // count of todos that are not complete
        this.remainingCount = ko.computed(function () {
            return this.todos().length - this.completedCount();
        }.bind(this));

        // writeable computed observable to handle marking all complete/incomplete
        this.allCompleted = ko.computed({
            //always return true/false based on the done flag of all todos
            read: function () {
                return !this.remainingCount();
            }.bind(this),
            // set all todos to the written value (true/false)
            write: function (newValue) {
                this.todos().forEach(function (todo) {
                    // set even if value is the same, as subscribers are not notified in that case
                    todo.completed(newValue);
                });
            }.bind(this)
        });

        // helper function to keep expressions out of markup
        this.getLabel = function (count) {
            return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items';
        }.bind(this);

        // internal computed observable that fires whenever anything changes in our todos
        ko.computed(function () {
            // store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
            localStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));
            alert(1);
        }.bind(this)).extend({
            rateLimit: { timeout: 500, method: 'notifyWhenChangesStop' }
        }); // save at most twice per second
    };
View Code

定義完成後,經過下述代碼,將ViewModel和view綁定起來

// bind a new instance of our view model to the page
    var viewModel = new ViewModel(todos || []);
    ko.applyBindings(viewModel);

存儲使用的是localStorage

// check local storage for todos
    var todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs'));

頁面連接路由用到了第三方插件

// set up filter routing
    /*jshint newcap:false */
    Router({ '/:filter': viewModel.showMode }).init();

3、backbone版todo主要內容

 從前文的文件結構中能夠發現,backbone版todo app包含index.html和collection部分、model部分、router部分、view部分這些子模塊js以及主模塊app.js

3.1 html文件index.html

<section id="todoapp">
            <header id="header">
                <h1>todos</h1>
                <input id="new-todo" placeholder="What needs to be done?" autofocus>
            </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 id="footer"></footer>
        </section>
        <footer id="info">
            <p>Double-click to edit a todo</p>
            <p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p>
            <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
        </footer>
        <script type="text/template" id="item-template">
            <div class="view">
                <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
                <label><%- title %></label>
                <button class="destroy"></button>
            </div>
            <input class="edit" value="<%- title %>">
        </script>
        <script type="text/template" id="stats-template">
            <span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
            <ul id="filters">
                <li>
                    <a class="selected" href="#/">All</a>
                </li>
                <li>
                    <a href="#/active">Active</a>
                </li>
                <li>
                    <a href="#/completed">Completed</a>
                </li>
            </ul>
            <% if (completed) { %>
            <button id="clear-completed">Clear completed (<%= completed %>)</button>
            <% } %>
        </script>
View Code

index.html中主要部份內容很簡潔,上述片斷中還包含了兩個模板的定義,若是隻看html部分,內容更少

<section id="todoapp">
            <header id="header">
                <h1>todos</h1>
                <input id="new-todo" placeholder="What needs to be done?" autofocus>
            </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 id="footer"></footer>
        </section>

上述html中,只有最基本的html元素和屬性

backbone沒有對html添加擴展屬性,對html是沒有侵入的

todo對象的列表,也頁面底部的狀態過濾連接,是經過view template插入到html中的

3.2 各個js文件分析

 app.js做爲backbone 業務代碼主模塊,內容很簡單,在頁面加載完以後,對AppView進行了實例化

/*global $ */
/*jshint unused:false */
var app = app || {};
var ENTER_KEY = 13;
var ESC_KEY = 27;

$(function () {
    'use strict';

    // kick things off by creating the `App`
    new app.AppView();
});

app-view.js是應用頂層的view,處理的對象是todo model的集合

在app-view.js代碼中,首先指定了視圖的做用對象和模板對象

而後在events對象中,爲dom元素特定事件綁定事件處理函數

在initialize對象中,爲todos集合綁定特定事件的事件處理函數

在render函數中,用模板對象渲染指定dom元素

隨後依次定義事件處理函數

 

和app-view.js不一樣,todo-view.js是負責處理todo list中單個todo對象的dom處理

todo-view.js中代碼過程與app-view.js中大體類似

更多view內容可參考What is a view?

 

todo.js定義了todo對象模型,而todos.js中定義了todo對象模型的集合

前文knockout版本todo app中,也有相應的todo對象和todos對象集合

相比knockout版本中的對象和集合,backbone版本中獨立出model和collection模塊的意義是什麼呢

答案是backbone中model和collection功能比knockout中豐富的多

model是js應用的核心,包括基礎的數據以及圍繞着這些數據的邏輯:數據轉換、驗證、屬性計算和訪問控制

collection是model對象的集合,爲model對象提供便捷的操做。在我看來,collection不是必須的,他屬於語法糖類型的東西。

更多model和collection內容能夠參考

Backbone入門指南(四):Model(數據模型)
Backbone入門指南(五):Collection (數據模型集合)

 

router.js是根據backbone內置的路由模塊實現的路由處理,根據All、Active、Completed三個不一樣連接,進行不一樣操做

router使用能夠參考認識 Backbone(三) : 什麼是 Router

4、angular版todo主要內容

 angular版本todo app包含index.html view文件和controller部分、director部分、service部分和主入口app.js

4.1 index.html分析

<ng-view />

        <script type="text/ng-template" id="todomvc-index.html">
            <section id="todoapp">
                <header id="header">
                    <h1>todos</h1>
                    <form id="todo-form" ng-submit="addTodo()">
                        <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus>
                    </form>
                </header>
                <section id="main" ng-show="todos.length" ng-cloak>
                    <input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
                    <label for="toggle-all">Mark all as complete</label>
                    <ul id="todo-list">
                        <li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
                            <div class="view">
                                <input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)">
                                <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
                                <button class="destroy" ng-click="removeTodo(todo)"></button>
                            </div>
                            <form ng-submit="saveEdits(todo, 'submit')">
                                <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo">
                            </form>
                        </li>
                    </ul>
                </section>
                <footer id="footer" ng-show="todos.length" ng-cloak>
                    <span id="todo-count"><strong>{{remainingCount}}</strong>
                        <ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
                    </span>
                    <ul id="filters">
                        <li>
                            <a ng-class="{selected: status == ''} " href="#/">All</a>
                        </li>
                        <li>
                            <a ng-class="{selected: status == 'active'}" href="#/active">Active</a>
                        </li>
                        <li>
                            <a ng-class="{selected: status == 'completed'}" href="#/completed">Completed</a>
                        </li>
                    </ul>
                    <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button>
                </footer>
            </section>
            <footer id="info">
                <p>Double-click to edit a todo</p>
                <p>Credits:
                    <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
                    <a href="http://ericbidelman.com">Eric Bidelman</a>,
                    <a href="http://jacobmumm.com">Jacob Mumm</a> and
                    <a href="http://igorminar.com">Igor Minar</a>
                </p>
                <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
            </footer>
        </script>
View Code

查看index.html發現,body元素下,第一行元素爲

<ng-view />

隨後,在腳本<script type="text/ng-template" id="todomvc-index.html"></script>中,定義了app的html

html屬性中,看到不少ng開頭的屬性,如ng-app,ng-submit,ng-model等

這些屬性,都是angular對html的擴展,而上述屬性中大部分是angular內置的指令

todo-escape,todo-focus這兩個不是以ng開頭的指令,是app自定義的指令

對ng-view指令的用法,更多內容可參考AngularJS Views

4.2 js業務代碼分析

 angular程序的啓動開始於ng-app指令,他的位置也決定了腳本的做用域範圍

<body ng-app="todomvc">

這裏註冊的todomvc模塊,與app.js中定義的模塊是一致的

angular.module('todomvc', ['ngRoute'])
    .config(function ($routeProvider) {
        'use strict';

        var routeConfig = {
            controller: 'TodoCtrl',
            templateUrl: 'todomvc-index.html',
            resolve: {
                store: function (todoStorage) {
                    // Get the correct module (API or localStorage).
                    return todoStorage.then(function (module) {
                        module.get(); // Fetch the todo records in the background.
                        return module;
                    });
                }
            }
        };

        $routeProvider
            .when('/', routeConfig)
            .when('/:status', routeConfig)
            .otherwise({
                redirectTo: '/'
            });
    });

應用程序入口,app.js中,定義了todomvc模塊,引入了ngRoute模塊

程序中,採用$routeProvider服務對頁面路由進行了配置,指定連接對應的配置中,控制器是 TodoCtrl,模板地址是todomvc-index.html,定義了resolve對象,根據todoStorage服務,獲取todos集合,填充store對象

關於這裏的路由配置中,配置對象和resolve用法,能夠參考Promise/Q和AngularJS中的resolve

 

todoCtrl.js是應用的控制器部分,控制器是和應用的與對象scope交互的地方,能夠將一個獨立視圖的業務邏輯封裝在一個獨立的容器中。

index.html的模板中,涉及的屬性和方法,都是在todoCtrl.js中定義的

 

todoFocus.js和todoEscape.js是兩個自定義指令,對應todo-focus和todo-escape

這裏的自定義指令,實際上能夠對應到knockout的custom binding,均是對內置指令的擴展

對指令的使用可參考《AngularJS》5個實例詳解Directive(指令)機制

 

todoStorage.js是應用的服務部分,服務部分提供了http服務和localStorage兩種方式,而且提供的是promise的異步處理方式

對promise的介紹和對angular中$q服務的介紹能夠參考

5、總結

單以此todo app來看knockout、backbone和angular

文件結構上

knockout最簡潔,angular其次,backbone最複雜

對html侵入上

backbone對html無侵入,knockout增長了data-bind屬性,angular增長了一套屬性並提供自定義屬性方法

對第三方插件依賴上

knockout不提供dom操做,不提供路由操做,提供簡單的模板支持

backbone不提供dom操做,提供了路由模塊,依賴underscore函數庫

angular提供內置的jqlite,提供路由服務,異步處理服務,依賴服務

代碼分離角度

我的認爲backbone和angular都比較清晰,knockout通常

相關文章
相關標籤/搜索