• 以todomvc为例分析knockout、backbone和angularjs


    一、整体结构

    项目github地址https://github.com/tastejs/todomvc/ 

    排除通用的css样式文件和引用的js库文件,仅看html和js

    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

    二、knockout版todo主要内容

     knockout版todo app实现细节,之前有文讲过,详情见《用KnockoutJS实现ToDoMVC代码分析》

    从上文的文件结构可知,其业务代码只有app.js,html view只有index.html

    2.1 视图代码index.html

    knockout在html原有属性基础上,新增了data-bind属性

    data-bind属性作为knockout与html交互的入口,内置了如下若干指令

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

    除了上述内置指令,knockout也可以添加自定义指令,如html中出现的enterKeyescapeKey和selectAndFocus指令

    <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做了定义

    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 model

    // 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中的业务方法和属性

    // 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();

    三、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

    四、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服务的介绍可以参考

    五、总结

    单以此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一般

  • 相关阅读:
    ps4 如何导出切片 单个图片
    测试webservice的时候,如果出现这个错误:"The test form is only available for requests from the local machine"
    js jquery 按钮点击后 60秒之后才能点击 60秒倒计时
    有空研究一下 superwebsocket (底层是 supersocket) 用来实现 web聊天什么的
    Vue学习笔记一:初识Vue
    被爬虫了,嘻嘻嘻
    Mybatis-generator自动生成器
    SpringCloud笔记五:Feign
    SpringCloud笔记四:Ribbon
    SpringCloud笔记三:Eureka服务注册与发现
  • 原文地址:https://www.cnblogs.com/GongQi/p/4728730.html
Copyright © 2020-2023  润新知