65.9K
CodeProject 正在变化。 阅读更多。
Home

无框架高治理 - 第二部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2016年6月14日

CPOL

11分钟阅读

viewsIcon

11034

downloadIcon

101

使用高级开发人员治理来实现一个无框架的客户端应用程序。

引言

本三部分系列文章《无框架高治理》的第二部分,通过在 第一部分 中描述的高治理,展示了如何将 TodoMVC 实现为一个无框架的应用程序。

背景

第一部分 本三部分系列文章探讨了开发无框架应用程序的背景、动机和架构方法。  第二部分 展示了一个无框架应用程序的实现。 第三部分 反驳了反对无框架应用程序开发的论点。

TodoMVC 实现

实现类建立在上一节介绍的基础类之上。TodosPresenter继承自基类Controller,该类提供了一个极其简单的路由机制。Todos封装了一个TodoView对象来管理视图,以及一个TodoModel对象来管理数据。它还包含一个TodoConfig对象,用于检索设置和其他配置值。

图 13:Todos Presenter 及其组件类。

Todos类实现封装了其内部依赖项,而不是像VanillaJS示例那样将依赖项注入到类中。封装遵循面向对象原则,即隐藏数据免受客户端影响并本地化更改。

此外,Todos实例的创建与其初始化是分开的。这是因为创建是分配表示实例的内存的过程,而初始化是获取资源的行为。创建始终是同步的。另一方面,初始化根据资源获取的方式,同步或异步发生。

/*jshint strict: true, undef: true, eqeqeq: true */
/* globals console, Promise, Subscriber, Controller, TodoConfig, TodoModel, TodoView */

/**
 * The todos controller.
 *
 * @class
 */
function Todos() {
    "use strict";
    
    this.inherit(Todos, Controller);
    
    var self = this,
        settings = new TodoConfig(),
        view = new TodoView(),
        model = new TodoModel();
    
    /**
     * Initialize instance.
     */
    function initialize() {
        view.on(subscribers);
        view.render(view.commands.initContent, settings);
        self.$base.init.call(self, router);
    }
    
    /**
     * Display the remaining number of todo items.
     */
    function showStats() {
        model.getStats(function(stats) {
            view.render(view.commands.showStats, stats);
            view.render(view.commands.toggleAll, (stats.completed === stats.total));
        });
    }
    
    /**
     * Initialize the todos controller.
     * 
     * @returns {Promise}   Resource acquisition promise.
     */
    this.init = function() {
        
        // Chained initialization: settings->model->view->initialize.
        return settings.init()
            .then(function() {
                return model.init();
            }).then(function() {
                return view.init();
            }).then(initialize);

        // Parallelize initialization:  (settings||model||view)->initialize.
        // Comment the chained implementation then uncomment the section below.
//        return Promise.all([settings.init(),
//                            model.init(),
//                            view.init()])
//            .then(initialize);
    };
    
    /**
     * Set the application state.
     *
     * @param {string} '' | 'active' | 'completed'
     */
    this.stateChanged = function() {
        this.executeState();
        view.render(view.commands.setFilter, this.getHyperlink());
        showStats();
    };
    
    /**
     * Router is the receiver of events that changes the application state.
     */
    var router = {
        
        /**
         * Renders all todo list items.
         */
        default: function () {
            model.find(function (results) {
                view.render(view.commands.showEntries, results);
            });
        },

        /**
         * Renders all active tasks
         */
        active: function () {
            model.find({ completed: false }, function (results) {
                view.render(view.commands.showEntries, results);
            });
        },

        /**
         * Renders all completed tasks
         */
        completed: function () {
            model.find({ completed: true }, function (results) {
                view.render(view.commands.showEntries, results);
            });
        }
    };
    
    
    /**
     * Subscriber of view events.
     */
    var subscribers = {
        
        /**
         * Adds a new todo item to the todo list.
         */
        todoAdd: new Subscriber(this, function (title) {

            // Add item.
            if (title.trim() === '') {
                return;
            }

            model.add(title, new Subscriber(this, function () {
                view.render(view.commands.clearNewTodo);
                this.stateChanged();
            }));
        }),

        /*
         * Starts the todo item editing mode.
         */
        todoEdit: new Subscriber(this, function (id) {
            model.find(id, function (results) {
                view.render(view.commands.editItem, id, results[0].title);
            });
        }),
        
        /*
         * Saves edited changes to the todo item.
         */
        todoEditSave: new Subscriber(this, function (id, title) {
            if (title.length !== 0) {
                model.save({id: id, title: title}, function (item) {
                    view.render(view.commands.editItemDone, item.id, item.title);
                });
            } else {
                subscribers.todoRemove(id);
            }
        }),

        /*
         * Cancels the todo item editing mode and restore previous value.
         */
        todoEditCancel: new Subscriber(this, function (id) {
            model.find(id, function (results) {
                view.render(view.commands.editItemDone, id, results[0].title);
            });
        }),
        
        /**
         * Removes the todo item.
         */
        todoRemove: new Subscriber(this, function (id, silent) {
            model.remove(id, function () {
                view.render(view.commands.removeItem, id);
            });

            if (!silent)
                showStats();
        }),
        
        /**
         * Removes all completed items todo items.
         */
        todoRemoveCompleted: new Subscriber(this, function () {
            model.find({ completed: true }, function (results) {
                results.forEach(function (item) {
                    subscribers.todoRemove(item.id, true);
                });
            });

            showStats();
        }),
        
        /**
         * Toggles the completion of a todo item.
         */
        todoToggle: new Subscriber(this, function (viewdata, silent) {
            model.save(viewdata, function (item) {
                view.render(view.commands.completedItem, item.id, item.completed);
            });
            
            if (!silent)
                showStats();
        }),
        
        /**
         * Toggles completion of all todo items.
         */
        todoToggleAll: new Subscriber(this, function (completed) {
            model.find({ completed: !completed }, function (results) {
                results.forEach(function (item) {
                    subscribers.todoToggle({id: item.id,
											title: item.title,
											completed: completed},
											true);                
                });
            });
            
            showStats();
        })
    };
}
图 14:Todos.js。

Todos类定义了一个router对象,其属性代表defaultactivecompleted这三种演示模式。default状态显示活动和已完成待办事项列表;active状态显示活动待办事项列表;completed状态显示已完成待办事项列表。subscribers对象将事件消息定义为属性,这些属性具有由视图触发的相应事件处理程序。

Todos类封装命令和事件。router类的状态被定义为命令。init方法注册Todos类的命令。subscribers对象定义了Todos类的事件消息和事件处理程序。view.on方法将Todos类订阅者附加到触发事件的TodoView类。

TodoMVC 模型

在MVP架构中,TodosPresenter控制模型状态,因为Todos会启动模型的所有操作。因此,TodoMVC应用程序不需要模型触发事件,尽管基类Model支持这一点。TodoModel类继承自Model类,并使用Storage类来执行从localStorage的数据访问的繁重工作。

图 15:TodoMVC 数据访问类。

待办事项列表会保存在浏览器本地存储中。TodoMVC使用的存储策略不是为每个待办事项存储一个键,而是将待办事项列表集合作为序列化的JSON数组进行持久化。这会将持久化减少到本地存储中的一个集合。

图 16:TodoMVC 使用一个键来存储每个集合的 localStorage 策略。

TodoMVC使用的查询仅限于按ID查找特定待办事项,以及查询活动或已完成待办事项列表。如果需要,可以增强Storage类来处理更复杂的查询,类似于MongoDB的风格。

/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/* globals $, console, document, Model, Subscriber, Storage, TodoItem */

/**
 * The todos storage model.
 *
 * @class
 */
function TodoModel() {
    "use strict";
    
    this.inherit(TodoModel, Model);
    
    var self = this,
        DBNAME = 'todos';
    
    /**
     * Initialize the model.
     * 
     * @returns {Promise}   Resource acquisition promise.
     */
    this.init = function() {
        return self.$base.init.call(self, DBNAME);
    };
    
    /**
     * Create new todo item
     *
     * @param {string} [title] The title of the task
     * @param {function} [callback] The callback to fire after the model is created
     */
    this.add = function(title, callback) {
        title = title || '';
        var todo = new TodoItem(title);
        self.$base.add.call(self, todo, callback);
    };
    
    
    /**
     * Returns a count of all todos
     */
    this.getStats = function(callback) {
        var results = self.$base.getItems.call(self);
        var stats = { active: 0, completed: 0, total: results.length};
        results.forEach(function(item) {
            if (item.value.completed) {
                stats.completed++;
            } else {
                stats.active++;
            }
        });

        callback(stats);
    };
    
    /**
     * Updates a model by giving it an ID, data to update, and a callback to fire when
     * the update is completed.
     *
     * @param {object}     entity      The properties to update and their new value
     * @param {function}   callback    The callback to fire when the update is complete.
     */
    this.save = function(entity, callback) {
        var todo = new TodoItem(entity.id, entity.title, entity.completed);
        self.$base.save.call(self, todo, callback);
    };
}
图 17:TodoModel.js。

TodoMVC 演示

演示系统协调TodosPresenter与TodoViewTodoTemplate类。Todos创建TodoView实例,该实例使用Todos事件订阅者进行初始化。TodoView接收用户事件并向用户显示信息。TodoView创建TodoTemplate,后者从模板化内容构建元素。

图 18:TodoMVC 演示类。

模板化

模板是动态创建并渲染到HTML中的内容片段,而不是在视图上静态渲染。模板引擎将模板转换为HTML内容。基类Template类使用Handlebars模板引擎将模板转换为HTML内容。

/*jshint strict: true, undef: true, laxbreak:true */
/* globals $, console, document, window, HTMLElement, Promise, Handlebars */

/**
 * The base template class.
 *
 * @class
 */
function Template() {
    "use strict";
    
    this.inherit(Template);
    
    var noop = function() {},
        templateCache = Object.create(null);
    
    /**
     * Converts relative url path to absolute url path.
     *
     * @param {string}     url     relative url path.
     *
     * @returns {string}           Absolute url.
     */
    function getAbsoluteUrl(relativeUrl) {
        var prefixIndex = window.location.href.lastIndexOf('/'),
            prefix = window.location.href.slice(0, prefixIndex+1);
        return prefix + relativeUrl;
    }
    
    /**
     * Load the template cache from the source properties.
     *
     * @param {object} source  Template source object.
     *
     * @returns {Promise}      Promise object.
     */
    function loadTemplateFromObject(source) {
        return new Promise(function(resolve, reject) {
            try {
                Object.getOwnPropertyNames(source).forEach(function(name) {
                    templateCache[name] = Handlebars.compile(source[name]);
                });
                
                if (Object.getOwnPropertyNames(templateCache).length > 0) {
                    resolve();
                } else {
                    reject({message: 'Cannot find template object'});
                }
            }
            catch(e) {
                reject(e);
            }
        }); 
    }
    
    /**
     * Load the template cache from the DOM.
     *
     * @param {jquery} source  DOM element containing templates.
     *
     * @returns {Promise}      Promise object.
     */
    function loadTemplateFromElement(source) {
        return new Promise(function(resolve, reject) {
            try {
                source.children().each(function(index, element) {
                    var name = element.getAttribute('id').replace('template-', '');
                    templateCache[name] = Handlebars.compile(element.innerHTML);
                });
                
                if (Object.getOwnPropertyNames(templateCache).length > 0) {
                    resolve();
                } else {
                    reject({message: 'Cannot find template source: (' + source.selector + ')'});
                }
            }
            catch(e) {
                reject(e);
            }
        }); 
    }
    
    /**
     * Retrieve templates from url.
     *
     * @param {string} source  The url of the tmeplates.
     *
     * @returns {Promise}      Promise object.
     */
    function loadTemplateFromUrl(source) {
        var lastSeparator = source.lastIndexOf('.'),
            name = source.substr(0, lastSeparator),
            ext = source.substr(lastSeparator) || '.html';
        
        return new Promise(function(resolve, reject) {
            try {
                    // load template file.
                    $.ajax({
                        url: name + ext,
                        dataType: 'text'
                    })
                    .done(function(data) {
                        
                        // find the template section.
                        var templateSection = $('#template-section');
                        if (!templateSection.length) {
                            templateSection = $(document.createElement('section'));
                            templateSection.attr('id', 'template-section');
                        }

                        templateSection.append($.parseHTML(data));
                        templateSection.children().each(function(index, element) {
                            var name = element.getAttribute('id').replace('template-', '');
                            templateCache[name] = Handlebars.compile(element.innerHTML);
                        });

                        templateSection.empty();
                        resolve();
                    })
                    .fail(function(xhr, textStatus, errorThrown) {
                        reject({xhr: xhr, 
                        message: 'Cannot load template source: (' + getAbsoluteUrl(name + ext) + ')',
                        status: textStatus});
                    });
            }
            catch(e) {
                reject(e);
            }
        }); 
    }
    
    /**
     * Retrieve templates from url.
     *
     * @param {$|HTMLElement|object|string}    source      Template source.
     * @param {function}                       callback    Loader callback.
     *
     * @returns {Promise}      Promise object.
     */
    function loadTemplate(source) {
        
        if (source instanceof $) {
            return loadTemplateFromElement(source);
        } else if (source instanceof HTMLElement) {
            return loadTemplateFromElement($(source));
        } else if (typeof source === "string") {
            return loadTemplateFromUrl(source);
        } else {
            return loadTemplateFromObject(source);
        }
    }
    
    /**
     * Retrieves the template by name.
     *
     * @param {string} name    template name.
     */
    function getTemplate(name) {
        return templateCache[name];
    }
    
    /**
     * Initialize the template
     *
     * @param {$|HTMLElement|object|string}    source      Template source.
     */
    this.init = function(source) {
        var self = this;
        return loadTemplate(source)
            .then(
                function (data) {
                    Object.getOwnPropertyNames(templateCache).forEach(function(name) {
                        Object.defineProperty(self, name, {
                            get: function() { return name; },
                            enumerable: true,
                            configurable: false
                        });
                    });
                });
    };
    
    /**
     * Create text using the named template.
     *
     * @param {string} name    Template name.
     * @param {object} data    Template data.
     *
     * @returns {string}       text.
     */
    this.createTextFor = function(name, data) {
        if (!name) return;
        var template = getTemplate(name);
        return template(data);    
    };
    
    /**
     * Create element using the named template.
     *
     * @param {string} name    Template name.
     * @param {object} data    Template data.
     *
     * @returns {$}            jQuery element.
     */
    this.createElementFor = function(name, data) {
        var html = this.createTextFor(name, data);
        var d = document.createElement("div");
        d.innerHTML = html;
        return $(d.children);
    };
}
图 19:Template.js。

TodoTemplate支持三种模板策略

  1. 在对象中定义的模板。

  2. 在HTML中定义的模板

  3. 在服务器上定义的模板。

TodoTemplateinit方法中使用的source参数决定了用于检索模板的策略。如果源是元素,则从HTML中获取模板。如果源是对象,则从对象的属性中检索模板。如果源是字符串,则假定它是URL路径,并从服务器检索模板。

在对象中定义的模板。

使用此策略,模板被定义为一个对象。每个单独的模板被标识为对象的属性。

/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/*global $, console, Template */

function TodoTemplate() {
    'use strict';
    
    this.inherit(TodoTemplate, Template);
    
    var self = this;
    
    /**
     * Initialize instance.
     */
    function initialize(source) {
        return self.$base.init.call(self, source)
            .catch(
                function (reason) {
                    console.log('Template cache load failure: ' + reason.message);
                });
    }
    
    /**
     * Template object.
     */
    var templates = {
        
        content: ''
                +   '<header class="header">'
                +       '<h1>todos</h1>'
                +       '<input class="new-todo" placeholder="{{placeholder}}" autofocus>'
                +   '</header>'
                +   '<section class="workspace">'
                +       '<section class="main">'
                +           '<input class="toggle-all" type="checkbox">'
                +           '<label for="toggle-all">{{markall}}</label>'
                +           '<ul class="todo-list"></ul>'
                +       '</section>'
                +       '<section class="menu">'
                +           '<span class="todo-count"></span>'
                +           '<ul class="filters">'
                +               '<li>'
                +                   '<a href="#/" class="selected">{{default}}</a>'
                +               '</li>'
                +               '<li>'
                +                   '<a href="#/active">{{active}}</a>'
                +               '</li>'
                +               '<li>'
                +                   '<a href="#/completed">{{completed}}</a>'
                +               '</li>'
                +           '</ul>'
                +        '<button class="clear-completed">{{clear}}</button>'
                +       '</section>'
                +   '</section>',
        
        listitem:   ''
                +   '<li data-id="{{id}}" class="{{completed}}">'
                +       '<div class="view">'
                +           '<input class="toggle" type="checkbox" {{checked}}>'
                +           '<label>{{title}}</label>'
                +           '<button class="destroy"></button>'
                +       '</div>'
                +   '</li>',
        
        summary:    '<span><strong>{{count}}</strong> item{{plural}} left</span>'
    };
    
    
    /**
     * init()
     *
     * initialize templates from source.
     *
     * @returns {Promise}   Promise used to acquire templates.
     */
    this.init = function() {
        return initialize(templates);
        //return initialize($('#templates'));
        //return initialize('template/templates.html');
    };
}
图 20:TodoMVC 模板在 TodoTemplate.js 中定义为对象。

嵌入HTML中的模板。

当前浏览器版本支持<template>元素,作为将模板直接嵌入HTML的方式。 对于旧版浏览器,开发人员可以使用<script>元素作为模板内容的替代品。 TodoMVC旨在在支持<template>元素的浏览器版本上运行。 为保持唯一标识符,模板名称前会加上template-

<!doctype html>
<html lang="en" data-framework="javascript">
    <head>
        <meta charset="utf-8">
        <title>TodoMVC</title>
        <link rel="stylesheet" href="css/base.css">
        <link rel="stylesheet" href="css/index.css">
    </head>
    <body>
        <section class="todoapp">
        </section>
        <footer class="info">
            <p>Double-click to edit a todo</p>
            <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p>
            <p>Refactored by <a href="https://github.com/cburgmer">Christoph Burgmer</a></p>
            <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
        </footer>
        <!-- -----------------------------------------------------------------------------  -->
        <!-- Content templates                                                              -->
        <!-- -----------------------------------------------------------------------------  -->
        <section id="templates">
            <!-- Content template -->
            <template id="template-content">
                <header class="header">
                    <h1>todos</h1>
                    <input class="new-todo" placeholder="{{placeholder}}" autofocus>
                </header>
                <section class="workspace">
                    <section class="main">
                        <input class="toggle-all" type="checkbox">
                        <label for="toggle-all">{{markall}}</label>
                        <ul class="todo-list"></ul>
                    </section>
                    <section class="menu">
                        <span class="todo-count"></span>
                        <ul class="filters">
                            <li>
                                <a href="#/" class="selected">{{default}}</a>
                            </li>
                            <li>
                                <a href="#/active">{{active}}</a>
                            </li>
                            <li>
                                <a href="#/completed">{{completed}}</a>
                            </li>
                        </ul>
                        <button class="clear-completed">{{clear}}</button>
                    </section>
                </section>
            </template>

            <!-- Todo list item template -->
            <template id="template-listitem">
                <li data-id="{{id}}" class="{{completed}}">
                    <div class="view">
                        <input class="toggle" type="checkbox" {{checked}}>
                        <label>{{title}}</label>
                        <button class="destroy"></button>
                    </div>
                </li>
            </template>

            <!-- Todos summary template -->
            <template id="template-summary">
                <strong>{{count}}</strong> item{{plural}} left
            </template>
        </section>
		<!—- scripts definitions removed for brevity (see Index.html) -->
    </body>
</html>
图 21:TodoMVC 模板定义使用<template>元素嵌入HTML中。

服务器上托管的模板。

最后一个关键策略是模板从服务器请求。有几种方法可以在服务器上组织模板并将其下载到客户端。本版TodoMVC中的策略是将模板组织成<template>元素内的源片段,位于服务器位置template/templates.html的单个文件中。使用ajax调用,包含模板片段的文件会被下载到客户端,然后这些片段会被转换为可用的<template>DOM元素。

<!-- Content template -->
<template id="template-content">
    <header class="header">
        <h1>todos</h1>
        <input class="new-todo" placeholder="{{placeholder}}" autofocus>
    </header>
    <section class="workspace">
        <section class="main">
            <input class="toggle-all" type="checkbox">
            <label for="toggle-all">{{markall}}</label>
            <ul class="todo-list"></ul>
        </section>
        <section class="menu">
            <span class="todo-count"></span>
            <ul class="filters">
                <li>
                    <a href="#/" class="selected">{{default}}</a>
                </li>
                <li>
                    <a href="#/active">{{active}}</a>
                </li>
                <li>
                    <a href="#/completed">{{completed}}</a>
                </li>
            </ul>
            <button class="clear-completed">{{clear}}</button>
        </section>
    </section>
</template>


<!-- Todo list item template -->
<template id="template-listitem">
    <li data-id="{{id}}" class="{{completed}}">
        <div class="view">
            <input class="toggle" type="checkbox" {{checked}}>
            <label>{{title}}</label>
            <button class="destroy"></button>
        </div>
    </li>
</template>


<!-- Todos summary template -->
<template id="template-summary">
    <span><strong>{{count}}</strong> item{{plural}} left</span>
</template>
图 22:服务器模板文件 template/template.html 包含模板定义。

TodoTemplate在运行时组装HTML模板,并将HTML演示与视图解耦。通过这种分离,TodoView的职责仅集中于视觉渲染和事件处理。

TodoView

TodoView管理应用程序的用户界面和外观。它将信息渲染到显示屏,并将用户事件转化为订阅者消息。TodoView订阅者由其对TodosPresenter类的所有权提供。这建立了TodosTodoView之间的操作模式,其中TodosTodoView发出命令,TodoView也向Todos发布消息。

下面的UML图显示了TodoView类的类层次结构和组成。

图 23:TodoView 类的结构。

作为Dispatcher类的后代,TodoView继承了传入命令和传出事件的实现。TodosPresenter通过render方法向TodoView发出命令,该方法使用TodoView DOM元素向用户显示内容。

当用户发出事件时,附加的DOM事件处理程序会处理用户事件。之后,TodoView会将用户事件转化为事件消息,并发布给订阅者。

使用此事件模型消除了耦合,因为事件消息的发布与其订阅者之间存在完全的自主性。另一方面,命令具有更紧密的耦合,因为客户端了解命令提供者。命令和事件的组合允许TodosPresenter向其依赖的TodoView发出命令。同时,TodoView通过订阅者以独立的方式将事件发布给Todos

下面的源代码显示了TodoView如何将所有内容整合在一起。在构造时,TodoView创建以下内容:

  1. 一个空对象作为DOM元素的占位符(dom

  2. TodoTemplate的实例

  3. 用于视图命令的容器对象(viewCommands

  4. 最后,它将viewCommands注册到其基类Dispatcher

Todos发出的initContent命令初始化TodoView。在initContent中,DOM元素被初始化,attachHandlers将事件处理程序连接到DOM元素。这些处理程序处理DOM事件,然后将其转化为转发给视图订阅者的消息。TodosPresenter定义了视图订阅者。

/*jshint strict: true, undef: true, eqeqeq: true */
/*global $, document, View, TodoTemplate */

/**
 * The todo view.
 *
 * @class
 */
function TodoView() {
    'use strict';
    
    this.inherit(TodoView, View);
    
    var self = this,
        view = {},
        todoapp = $('.todoapp'),
        template = new TodoTemplate(),
        
        viewCommands = {
            
            initContent: function(settings) {
                var element = template.createElementFor(template.content, settings.glossary);
                todoapp.append(element);
                view.todoList = $('.todo-list');
                view.todoItemCount = $('.todo-count');
                view.clearCompleted = $('.clear-completed');
                view.workspace = $('.workspace');
                view.main = $('.main');
                view.menu = $('.menu');
                view.toggleAll = $('.toggle-all');
                view.newTodo = $('.new-todo');
                attachHandlers();
            },
            
            showEntries: function (todos) {
                view.todoList.empty();
                todos.forEach(function(todo) {
                    var viewdata = Object.create(null);
                    viewdata.id = todo.id;
                    viewdata.title = todo.title;
                    if (todo.completed) {
                        viewdata.completed = 'completed';
                        viewdata.checked = 'checked';
                    }

                    var element = template.createElementFor(template.listitem, viewdata);
                    view.todoList.append(element);
                });
            },
            
            showStats: function (stats) {
                var viewdata = Object.create(null);
                viewdata.count = stats.active;
                viewdata.plural = (stats.active > 1) ? 's' : '';
                var text = template.createTextFor(template.summary, viewdata);
                view.todoItemCount.html(text);
                view.workspace.css('display', (stats.total > 0) ? 'block' : 'none');
                view.clearCompleted.css('display', (stats.completed > 0) ? 'block' : 'none');
            },
            
            toggleAll: function (isCompleted) {
                view.toggleAll.prop('checked', isCompleted);
            },
            
            setFilter: function (href) {
                view.menu.find('.filters .selected').removeClass('selected');
                view.menu.find('.filters [href="' + href + '"]').addClass('selected');
            },
            
            /**
             * Clears the new todo field.
             */
            clearNewTodo: function () {
                view.newTodo.val('');
            },
            
            /**
             * Change the completion state of the todo item.
             *
             * @param {number} id       The todo identifier.
             * @param {string} title    The title of the todo.
             */
            completedItem: function (id, completed) {
                var listItem = view.todoList.find('[data-id="' + id + '"]');
                var btnCompleted = listItem.find('.toggle');
                listItem[(completed) ? 'addClass' : 'removeClass']('completed');
                btnCompleted.prop('checked', completed);
            },
            
            /**
             * Edit todo by creating an input field used for editing.
             *
             * @param {number} id       The todo identifier.
             * @param {string} title    The title of the todo.
             */
            editItem: function (id, title) {
                var listItem = view.todoList.find('[data-id="' + id + '"]'),
                    input = $(document.createElement('input'));
                listItem.addClass('editing');
                input.addClass('edit');
                listItem.append(input);
                input.val(title);
                input.focus();
            },
            
            /**
             * Edit of todo is completed.
             *
             * @param {number} id       The todo identifier.
             * @param {string} title    The title of the todo.
             */
            editItemDone: function (id, title) {
                var listItem = view.todoList.find('[data-id="' + id + '"]');
                listItem.find('input.edit').remove();
                listItem.removeClass('editing');
                listItem.removeData('canceled');
                listItem.find('label').text(title);
            },
            
            /**
             * Remove the todo item.
             *
             * @param {number} id       The todo identitifier.
             */
            removeItem: function (id) {
                var item = view.todoList.find('[data-id="' + id + '"]');
                item.remove();
            }
        };
    
    /**
     * Initialize instance.
     */
    function initialize() {
        self.$base.init.call(self, viewCommands);
    }
    
    /**
     * Attaches the UI event handler to the view selectors.
     */
    function attachHandlers() {
        
        view.newTodo.on('change', function() {
            self.trigger(self.messages.todoAdd, this.value);
        });

        view.clearCompleted.on('click', function() {
            self.trigger(self.messages.todoRemoveCompleted, this, view.clearCompleted.checked);
        });


        view.toggleAll.on('click', function(event) {
            self.trigger(self.messages.todoToggleAll, view.toggleAll.prop('checked'));
        });

        /**
         * Initiate edit of todo item.
         *
         * @param {event}   event   Event object.
         */
        view.todoList.on('dblclick', 'li label', function(event) {
            var id = $(event.target).parents('li').data('id');
            self.trigger(self.messages.todoEdit, id);
        });

        /**
         * Process the toggling of the completed todo item.
         *
         * @param {event}   event   Event object.
         */
        view.todoList.on('click', 'li .toggle', function(event) {
            var btnCompleted = $(event.target);
            var todoItem = btnCompleted.parents('li');
            var label = todoItem.find('label');
            self.trigger(self.messages.todoToggle, {id: todoItem.data('id'), title: label.text(), completed: btnCompleted.prop('checked')});
        });

        /**
         * Accept and complete todo item editing.
         *
         * @param {event}   event   Event object.
         */
        view.todoList.on('keypress', 'li .edit', function(event) {
            if (event.keyCode === self.ENTER_KEY) {
                $(event.target).blur();
            }
        });

        /*
         * Cancel todo item editing.
         */
        view.todoList.on('keyup', 'li .edit', function(event) {
            if (event.keyCode === self.ESCAPE_KEY) {
                var editor = $(event.target);
                var todoItem = editor.parents('li');
                var id = todoItem.data('id');
                todoItem.data('canceled', true);
                editor.blur();
                self.trigger(self.messages.todoEditCancel, id);
            }
        });

        /*
         * Complete todo item editing when focus is loss.
         */
        view.todoList.on('blur', 'li .edit', function(event) {
            var editor = $(event.target);
            var todoItem = editor.parents('li');
            if (!todoItem.data('canceled')) {
                var id = todoItem.data('id');
                self.trigger(self.messages.todoEditSave, id, editor.val());
            }
        });

        // Remove todo item.
        view.todoList.on('click', '.destroy', function(event) {
            var id = $(event.target).parents('li').data('id');
            self.trigger(self.messages.todoRemove, id);
        });
    }
    
    /**
     * Initialize the view.
     * 
     * @returns {Promise}   Resource acquisition promise.
     */
    this.init = function() {
        return template.init()
            .then(initialize);
    };
}
图 24:TodoView.js。

初始化模式

对象构造过程是面向对象编程的基础。在对象构造期间,分配对象的内存表示,然后执行对象的构造函数方法。面向对象语言通过new运算符支持对象构造。这种内在支持掩盖了对象构造的两个不同的任务——对象创建和对象初始化

对象创建是分配对象内存表示的过程,此过程同步执行。对象初始化是为对象分配其状态和获取要使用的资源的过程。对象初始化的执行是同步的或异步的。new运算符使用同步初始化,这会倾向于将对象构造过程视为完全同步的。

当初始化数据易于获得且分配时间可忽略时,同步初始化最适合。一个常见的例子是从默认值分配对象状态。同步初始化在使用上变得不切实际,特别是对于在初始化过程中获取资源的对象的。资源获取的时间跨度并非总是可以忽略的,并且在同步运行时会阻塞CPU。从服务器获取的资源使用异步请求。发出异步资源请求的同步初始化需要一个等待机制,以防止应用程序在请求完成之前继续运行。没有等待机制,应用程序将返回一个状态未定义的未初始化对象。为了能够正确处理资源获取,需要分离对象初始化职责,从而导致异步初始化模式。

TodoMVC实现了遵循以下规则的替代初始化模式:

  • 类构造函数仅限于对象创建和同步状态分配。

  • 类通过名为init的方法执行对象初始化。

  • init方法返回一个异步对象。

  • 嵌套初始化程序可确保内部类的初始化先于外部类。

  • 异步对象的链接可保证初始化序列顺序。

下图表示了初始化模式的模型。

图 25:TodoMVC 初始化模式。

如图所示,初始化顺序为OuterClass.Class1InnerClassOuterClass.Class2OuterClass.Class3OuterClassOuterClass的客户端通过调用init方法开始初始化。OuterClassinit方法以异步链的形式调用其所有内部类的init方法。链接可确保内部兄弟类别的初始化调用顺序。

JavaScript Promise

JavaScript使用Promise类来创建一个返回未来结果的代理的异步对象。对Promises的详细讨论超出了本文的范围,但有初步了解有助于解释TodoMVC中使用的初始化模式。要获得详细解释,请访问ExploringJS网站。

Promise构造函数有一个名为executor的函数参数。executor是一个包含异步操作的函数,该异步操作由Promise对象执行。executor函数有两个回调函数作为参数:resolvereject,Promise对象分别通过它们返回成功错误的结果。

function asyncFunc() {
    return new Promise (
        function(resolve, reject) { // Promise executor.

            resolve(value); // returns success value of the Promise executor.

            reject(error); // returns error value in case of failure.
    });
}
图 26:返回 Promise 的异步函数。

下面显示的.js代码演示了异步初始化模式的实现。它展示了初始化序列如何由Promise对象的链式调用来管理。链式调用通过Promisethen方法建立操作的延续序列。延续是一个在Promise executor成功完成后执行的回调。then方法本身返回一个Promise对象,这使得能够将延续链接成一个序列。

/*
 * Asynchronous initialization pattern – InnerClass.
 *
 * @class
 */
function InnerClass() {
    "use strict";
    
    this.inherit(InnerClass);
    
    var self = this,
        class1 = new Class1();
    
    /**
     * Initialize InnerClass internal state.
     */
    function initialize() {
        ...
    }


    /**
     * Initialize InnerClass.
    * 
    * @returns {Promise}   Initialize contained objects.
     */
    this.init = function() {
        return Class1.init()
            .then(initialize);
    };
}


/*
 * Asynchronous initialization pattern – OuterClass.
*
 * @class
 */
function OuterClass() {
    "use strict";
    
    this.inherit(OuterClass);
    
    var self = this,
        innerClass = new InnerClass(),
        class2 = new Class2(),
        class3 = new Class3();
    
    /**
     * Initialize OuterClass internal state.
     */
    function initialize() {
        ...
    }


    /**
     * Initialize OuterClass.
    * 
    * @returns {Promise}   Initialization of contained objects via chaining.
     */
    this.init = function() {
        return InnerClass.init()
            .then(function() {
                return class2.init();
            }).then(function() {
                return class3.init();
            }).then(initialize);
    };
}
图 27:异步初始化模式的实现。

使用TodoMVC中使用的对象的这种实现模式可确保应用程序中对象初始化的Consistency。在初始化过程中,TodoView.init调用TodoTemplateinit方法,该方法返回一个Promise。由TodoTemplate返回的Promisethen方法包含一个延续函数(initialize),该函数在TodoTemplate初始化完成后被调用。TodoView的initialize方法调用基类View.init方法来完成视图的初始化。

function TodoView() {
    'use strict'; 
    
    this.inherit(TodoView, View); 
    
    var self = this, 
        view = {},
        todoapp = $('.todoapp'), 
        template = new TodoTemplate(),

        viewCommands = {
             ...
        };


    /**
     * Initialize instance.
     */
    function initialize() {
        self.$base.init.call(self, viewCommands);
    }


    /**
     * Initialize the view.
     * 
     * @returns {Promise}   Resource acquisition promise.
     */
    this.init = function() {
        return template.init()
            .then(initialize); 
    };
}
图 28:TodoView 初始化模式的实现。

作为TodoMVC中最外层的容器对象,Todos控制器的初始化通过其init方法触发所有包含对象(controller)的初始化。Todos控制器使用链式调用来顺序初始化其包含的对象。但是,如果包含对象的初始化不依赖于顺序,则并行化提供了一种替代链式调用的方法。Promise.all命令接受一个Promise数组,并行执行它们,并在所有Promise完成之前继续执行。并行化对象初始化提高了初始化过程的性能,并应与顺序初始化进行测试,以确定最适合应用程序的方法。

/**
 * The todos controller.
 *
 * @class
 */
function Todos() {
    "use strict";
    
    this.inherit(Todos, Controller);
    
    var self = this, 
        settings = new TodoConfig(),
        view = new TodoView(),
        model = new TodoModel();
    
    /**
     * Initialize instance.
     */
    function initialize() {
        view.on(subscribers);
        view.render(view.commands.initContent, settings);
        self.$base.init.call(self, router);
    }


    /**
     * Initialize the todos controller.
     * 
     * @returns {Promise}   Resource acquisition promise.
     */
    this.init = function() {

        // Chained initialization: settings->model->view->initialize.
        return settings.init()
            .then(function() {
                return model.init();
            }).then(function() {
                return view.init();
            }).then(initialize);

        // Parallelize initialization:  (settings||model||view)->initialize.
        // Comment the chained implementation then uncomment the section below.
//        return Promise.all([settings.init(),
//                            model.init(),
//                            view.init()])
//            .then(initialize);
    };
}
图 29:Todos 控制器的初始化模式实现。

使用代码

将zip文件下载到您的本地计算机并解压todomvc文件夹。要运行该应用程序,请在浏览器中打开index.html。使用浏览器的调试器分析该应用程序。

文件server.py设置了一个快速简陋的服务器环境。要从服务器运行该应用程序,请执行以下操作:

  • 在您的机器上安装python 3.0或更高版本。

  • 打开命令窗口。

  • 导航到todomvc文件夹。

  • 在命令行中键入python server.py以启动服务器

  • 在浏览器的地址栏中使用https://:8000来启动应用程序。

关注点

在工作实现到位后,在第三部分中,我们可以回顾并反驳反对无框架的论点。

历史

2016年6月11日 无框架实现初次演示。
2016年6月19日 修复了图20中JavaScript代码的可显示HTML。

© . All rights reserved.