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

无框架的高级治理 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (14投票s)

2016年6月14日

CPOL

20分钟阅读

viewsIcon

16964

一种无框架的Web开发方法,可实现高水平的开发人员治理。

引言

在最近的一个项目中,开发团队就是否应该采用流行的JavaScript框架来构建前端Web客户端存在争议。争议始于我的同事“Dale”,一位极其熟练的前端开发人员,他建议开发团队采用“无框架”方法。不用说,他的提议激怒了其他主张使用AngularJS的团队成员。

我对Dale的建议的最初反应是,无框架是偏离常规的激进做法,因为如今使用流行的JavaScript框架开发Web前端似乎是常见做法。但当我思考时,我回想起自己之前在一个项目中对AngularJS的评估,我得出结论,AngularJS如此掩盖了标准Web编程,以至于这种范式最好被称为*AngularJS编程*。

我对之前项目中的Backbone.js也很熟悉。与AngularJS不同,Backbone使用起来感觉更自然。Backbone是一个有助于将应用程序结构化为MV*架构模式的库,它利用了现有的编程知识。Backbone不会以迫使您学习新编程范式的方式发生偏离。正是出于这些原因,我站在了Dale这边。

不幸的是,今天,理性的论证、经验和常识推理并不总是能占上风。相反,诡辩常常获胜。我们的无框架反对者巧妙地包装了他们的论点,如下所示:

业务需求与架构考量

jQuery + 库

AngularJS

数据绑定
(双向)

 

 

 

 

 

涉及使用多个库,这增加了不稳定性。

 

这将需要手动进行DOM操作以移除和添加节点。这依赖于元素ID和类的使用。

 

 

内置支持双向数据绑定。

使用模型自动修改绑定的元素。

无需手动DOM操作。

无需手动DOM操作。

模板化

 

 

 

 

模板是文本文件,不可维护且不支持IntelliSense。

 

文本模板容易出错,因为模板是基于字符串的。

内置支持HTML模板和指令。
 

使用包含部分视图的HTML模板,可维护并支持IntelliSense。

MVC/MVVM解决方案设计

 

这种模式带来了清晰的关注点分离,使代码易于构建、维护和测试。

使用jQuery需要定义专有模式来实现MVC架构设计。这既耗时又不必要(重复造轮子)。

 

 

 

AngularJS具有内置的MVC架构,模块、控制器、服务和数据模型定义清晰。

 

 

 

依赖管理

 

 

无内置依赖管理。

此功能依赖于使用RequireJS或Inject等额外库,这增加了不稳定性。

AngularJS具有内置依赖管理。



 
单元测试

 

 





 

测试需要QUnit等多个库以及用于依赖注入等的其他库。

 

 

 

 

AngularJS具有内置依赖注入功能,可模拟单元测试的依赖项。

 

AngularJS单元测试可以使用Jasmine中的行为驱动设计模式编写,易于维护和阅读。

端到端测试或页面行为验证
jQuery无法实现。 AngularJS有一个名为Protractor的端到端测试框架。
合规性与许可

 
使用多个库将增加验证这些库的合规性和许可条件的巨大工作量和成本。 符合PCI和ISO标准的企业在市场中使用AngularJS,这减少了工作量和成本。
人员配置
 
难以找到合格的JavaScript开发人员。 AngularJS开发人员数量众多,并且拥有社区支持。
一致性
 
JavaScript编程需要指南来强制执行编程一致性。 AngularJS提倡一种适合开发团队的一致编程风格。
安全性与兼容性

 

 

 

 

 

 

 

使用多个库增加了确保它们相互兼容的负担。

使用多个库和手工编写的HTML/DOM操作增加了安全风险,这将增加额外安全审计的成本和工作量。

 

 

 

AngularJS是一个经过良好测试的框架,其设计目标是可测试性。

内置组件协同良好。

AngularJS被Google和其他企业使用,他们定期为框架贡献安全补丁。

安全审计所需的工作量和成本将大大减少。

反对我们立场的论点并非不真实或事实不符。相反,反对意见源于巧妙地将JavaScript库和开发者治理的优势转化为感知到的弱点,并将“担忧”转化为主要需求。我们的反驳显得苍白无力且具有防御性——不像他们积极主张使用AngularJS。他们的推理是这样的:“jQuery是否支持特性x、y和z”?答案是“否”,导致他们得出结论,AngularJS比jQuery具有更多特性,因此,AngularJS是更好的选择,于是我们输掉了辩论。

库提供服务或离散功能。因此,库通过允许开发者以最小的决策摩擦使用他们现有的技能和知识来维护和增强治理。另一方面,框架倾向于一致性和不透明性。因此,框架将治理从开发者身上转移开,造成不必要的摩擦,这会使高治理的开发者感到沮丧,无论框架提供了多少功能。

输掉这场辩论后,我不得不重新审视并质疑为什么Dale的立场变得如此尖刻,并决定将来如何妥善处理反对意见。使用JavaScript库开发应用程序需要哪些结构和策略?有人可能会争辩说:“这不是在‘重复造轮子’吗?”我会说,这是一种看法。但钢带式子午线轮胎的发明者难道不是“重新发明者”吗?这阻止他们了吗?花时间维护和增强治理是否值得?我相信,增强您对软件架构的知识,并好奇地理解引擎盖下发生的事情,会使您成为更好的开发人员。

因此,本文的动机并非是辩论AngularJS等框架的技术优缺点。本文的主要目的是增强开发人员的治理能力。促进更高治理意味着通过利用开发人员的技能和经验,赋予他们更大的自主权来行使权力和决策。框架通过锁定范式和一致性来降低治理,而库则促进更高的治理,因为它们依赖于开发人员的决策能力才能善加利用。

应用程序

开发的第一步是应用程序定义。一个很好的规范来源是 TodoMVC 网站。该网站提供了 Todo 列表管理器的规范。有几个使用 MV* 框架(如 AngularJS、Backbone.js、EmberJS 和 React)编写的 TodoMVC 项目示例。也有不使用任何框架编写的 TodoMVC 示例:一个 jQuery 版本和一个 VanillaJS 版本。VanillaJS 是不使用任何 JavaScript 库或框架编写的 TodoMVC 应用程序示例。

规格

TodoMVC 是一个简单的任务列表管理器。它允许用户输入每个任务,并呈现一个任务列表。用户可以添加、删除、编辑任务,切换特定任务或整个任务列表的完成状态,以及删除列表中所有已完成的任务。该应用程序还会显示剩余活动任务数量的摘要,并过滤活动和已完成的任务。TodoMVC 的屏幕截图如下所示。

图1:TodoMVC应用程序

TodoMVC 拥有相当直接而简洁的规范。批评者会争辩说这不是一个“真实世界”的应用程序。虽然TodoMVC确实不是一个企业级应用程序,但我们的开发方法使用了真实世界的策略和架构模式来展示高治理开发。我们的目标是让您,开发人员,保留对架构和开发过程的控制。

策略

现在我们理解了规范,下一步是应用程序架构。我们可以用两种方式来处理架构:

1.    满足此应用程序的狭隘要求。

2.    以MV*方式使用关注点分离来设计应用程序。

由于我们的目标是解决针对不使用框架的批评,因此首选的方法是将我们的 TodoMVC 版本设计为 MV* 应用程序。因为我们曾被批评“重复造轮子”,所以让我们看看是否可以利用一个现有版本的不使用框架编写的 TodoMVC。

TodoMVC.com 有两个作为无框架应用程序编写的版本:jQuery 和 VanillaJS。jQuery 应用程序使用了两个额外的库:Handlebars(用于模板处理)和 Director(用于路由)。

让我们看看jQuery代码。

/*global jQuery, Handlebars, Router */
jQuery(function ($) {
    'use strict'; 

    Handlebars.registerHelper('eq', function (a, b, options) {
        return a === b ? options.fn(this) : options.inverse(this); 
    });

    var ENTER_KEY = 13; 
    var ESCAPE_KEY = 27; 

    var util = {
        uuid: function () {
            /*jshint bitwise:false */
            var i, random; 
            var uuid = ''; 

            for (i = 0; i < 32; i++) {
                random = Math.random() * 16 | 0; 
                if (i === 8 || i === 12 || i === 16 || i === 20) {
                    uuid += '-'; 
                }
                uuid += (i === 12
                        ? 4 
                        : (i === 16 
                            ? (random & 3 | 8) 
                            : random)).toString(16); 
            }

            return uuid; 
        },

        pluralize: function (count, word) {
            return count === 1 ? word : word + 's'; 
        },

        store: function (namespace, data) {
            if (arguments.length > 1) {
                return localStorage.setItem(namespace, JSON.stringify(data)); 
            } else {
                var store = localStorage.getItem(namespace); 
                return (store && JSON.parse(store)) || [];
            }
        }
    };

    var App = {
        init: function () {
            this.todos = util.store('todos-jquery'); 
            this.todoTemplate = Handlebars.compile($('#todo-template').html());
            this.footerTemplate = Handlebars.compile($('#footer-template').html());
            this.bindEvents();

            new Router({
                '/:filter': function (filter) {
                    this.filter = filter; 
                    this.render();
                }.bind(this) 
            }).init('/all'); 
        },

        bindEvents: function () {
            $('#new-todo').on('keyup', this.create.bind(this)); 
            $('#toggle-all').on('change', this.toggleAll.bind(this)); 
            $('#footer').on('click',
                            '#clear-completed',
                            this.destroyCompleted.bind(this));
            $('#todo-list') 
                .on('change', '.toggle', this.toggle.bind(this)) 
                .on('dblclick', 'label', this.edit.bind(this)) 
                .on('keyup', '.edit', this.editKeyup.bind(this)) 
                .on('focusout', '.edit', this.update.bind(this)) 
                .on('click', '.destroy', this.destroy.bind(this)); 
        },

        render: function () {
            var todos = this.getFilteredTodos();
            $('#todo-list').html(this.todoTemplate(todos)); 
            $('#main').toggle(todos.length > 0); 
            $('#toggle-all').prop('checked', this.getActiveTodos().length === 0); 
            this.renderFooter();
            $('#new-todo').focus();
            util.store('todos-jquery', this.todos); 
        },

        renderFooter: function () {
            var todoCount = this.todos.length; 
            var activeTodoCount = this.getActiveTodos().length; 
            var template = this.footerTemplate({
                activeTodoCount: activeTodoCount, 
                activeTodoWord: util.pluralize(activeTodoCount, 'item'), 
                completedTodos: todoCount - activeTodoCount, 
                filter: this.filter
            });

            $('#footer').toggle(todoCount > 0).html(template); 
        },

        toggleAll: function (e) {
            var isChecked = $(e.target).prop('checked'); 

            this.todos.forEach(function (todo) {
                todo.completed = isChecked; 
            });

            this.render();
        },

        getActiveTodos: function () {
            return this.todos.filter(function (todo) {
                return !todo.completed; 
            });
        },

        getCompletedTodos: function () {
            return this.todos.filter(function (todo) {
                return todo.completed; 
            });
        },

        getFilteredTodos: function () {
            if (this.filter === 'active') {
                return this.getActiveTodos();
            }

            if (this.filter === 'completed') {
                return this.getCompletedTodos();
            }

            return this.todos; 
        },

        destroyCompleted: function () {
            this.todos = this.getActiveTodos();
            this.filter = 'all'; 
            this.render();
        },

        // accepts an element from inside the `.item` div and
        // returns the corresponding index in the `todos` array
        indexFromEl: function (el) {
            var id = $(el).closest('li').data('id'); 
            var todos = this.todos; 
            var i = todos.length; 

            while (i--) {
                if (todos[i].id === id) {
                    return i; 
                }
            }
        },

        create: function (e) {
            var $input = $(e.target); 
            var val = $input.val().trim();

            if (e.which !== ENTER_KEY || !val) {
                return; 
            }

            this.todos.push({
                id: util.uuid(),
                title: val, 
                completed: false
            });

            $input.val(''); 
            this.render();
        },

        toggle: function (e) {
            var i = this.indexFromEl(e.target); 
            this.todos[i].completed = !this.todos[i].completed; 
            this.render();
        },

        edit: function (e) {
            var $input = $(e.target).closest('li').addClass('editing').find('.edit');
            $input.val($input.val()).focus();
        },

        editKeyup: function (e) {
            if (e.which === ENTER_KEY) {
                e.target.blur();
            }

            if (e.which === ESCAPE_KEY) {
                $(e.target).data('abort', true).blur();
            }
        },

        update: function (e) {
            var el = e.target; 
            var $el = $(el); 
            var val = $el.val().trim();

            if (!val) {
                this.destroy(e); 
                return; 
            }

            if ($el.data('abort')) {
                $el.data('abort', false); 
            } else {
                this.todos[this.indexFromEl(el)].title = val; 
            }

            this.render();
        },

        destroy: function (e) {
            this.todos.splice(this.indexFromEl(e.target), 1); 
            this.render();
        }
    };

    App.init();
});
图2:TodoMVC的jQuery版本

好了,各位,就这样,我们完成了!好吧,但还没完全结束。虽然这段代码非常简洁并满足了规范,但其结构与实现紧密耦合。我们的目标是实现松耦合,并支持可重用且可扩展到未来项目的MV*架构。

仔细查看VanillaJS的实现更符合我们的喜好。讽刺的是,尽管存在无框架的争议,VanillaJS版本的代码结构是一个真正不使用框架或库编写的MVC应用程序。

因此,我们可以利用VanillaJS并仍然宣告胜利。但为了从这次练习中学习,最好对 TodoMVC 进行天真的评估。假设我们没有遇到VanillaJS或jQuery版本,但我们仍然希望采用无框架。我们该如何着手开发?

MV* 架构

MV* 代表一系列架构模式,它们将应用程序关注点分离为离散组件:模型(Model)、视图(View)和控制器(Controller)。*模型*管理应用程序使用的数据、业务对象和数据表示。*视图*呈现数据,并决定应用程序的外观。*控制器*调解模型和视图之间的交互。控制器的角色因架构模式而异。

模型-视图-控制器 (MVC)

在MVC模式中,用户与视图交互,视图将用户事件发送给控制器,控制器更新模型。模型状态改变时,通知视图,视图查询模型以渲染数据。

图3:模型-视图-控制器

模型-视图-表示器 (MVP)

在 MVP 模式中,控制器更好地被称为*表示器*,就像“呈现”模型对视图的更改一样。与 MVC 不同,模型和视图之间的所有交互都由表示器管理。模型将其状态更改通知表示器。然后,表示器向视图发出命令以呈现数据。表示器充当协调者,以维护模型和视图的分离和隔离,并禁止它们之间发生任何耦合。

 

图4:模型-视图-表示器

 

模型-视图-视图模型 (MVVM)

在MVVM模式中,控制器变成了*视图模型*,就像“视图的模型”一样。视图模型和视图通过数据绑定连接。数据绑定是一种互惠事件模式,它通知视图视图模型的变化,反之亦然。数据绑定层隐藏了视图模型和视图之间绑定方面和事件映射的细节。

在MVVM中,视图元素和事件处理程序是使用绑定语法声明性定义的。该语法由绑定层解释,该层自动“连接”UI元素、事件处理程序和视图模型的属性。数据绑定层执行的这项工作减轻了开发人员手动编写此工作的负担。

尽管数据绑定有许多优点,但它也有缺点。通常,数据绑定是一个“黑匣子”,当需要超出其假设和限制的功能时,它会变得不灵活。数据绑定代码通常不透明且专有,这使得调试和定制变得困难。

图5:模型-视图-视图模型

TodoMVC 架构

在上述MV*架构中,MVP模式是三者中最受青睐的。我做出这一决定的主要原因是:其实现透明度、模型和视图的内在隔离,以及模型和视图之间的所有交互都必须通过表示器。

演示器是应用程序的“大脑”,它封装了协调交互的逻辑。这使得组件的逻辑分离更容易理解、开发和维护。

MVC 之所以不足,是因为视图和模型之间并非完全隔离。在 MVVM 中,声明式数据绑定隐藏了视图和视图模型之间大量的交互细节。数据绑定的“幕后”活动降低了治理,这可能会成为开发的真正障碍,并阻碍调试。在这里,对透明度的偏好胜过 MVVM 的优势,即使这种偏好给开发人员带来了额外的负担。

TodoMVC 设计

既然我们已经决定采用 MVP 架构,那么让我们来考虑设计。 TodoMVC 使用面向对象设计。使用面向对象设计意味着必须识别对象及其交互。MVP 架构识别出三个对象:控制器(表示器)、视图和模型。然而,构建应用程序还需要其他对象。但首先让我们了解面向对象设计和面向对象编程。

面向对象

面向对象是一种围绕对象概念构建软件应用程序的方法。对象是一种抽象,包含表示对象特征的*属性*以及提供对象行为的*方法*。定义面向对象编程(OOP)有四个特征:

  • 抽象数据类型
    抽象数据类型代表一组属性和行为的分类。当被实例化时,会创建一个特定分类的对象。另一种思考方式是,抽象数据类型定义了用于创建实际对象的模式。

  • 封装
    封装代表数据和方法包含在一个离散的“胶囊”中。数据对客户端隐藏,只能通过属性和方法访问。

  • 继承
    继承允许定义从重用基类中定义的属性和方法派生出的新类。派生类以“is-a”关系排列,即这些类是基类的后代。派生类可以选择重用或覆盖基类功能。

  • 多态
    多态性是指方法具有不同实现,但可以使用相同签名。例如,在动物层次结构中,`Animal` 基类定义了 `speak` 方法。`Dog` 和 `Cat` 派生自 `Animal` 类并继承 `Animal` 的 `speak` 方法。当在 `Dog` 实例上调用 `speak` 方法时,它返回字符串 `"bark!"`。当从 `Cat` 实例调用 `speak` 方法时,返回字符串 `"meow!"`。

    在面向对象编程中,多态性通过消息协议实现。在上述示例中,消息协议是 `speak` 方法的签名(参见图7)。

JavaScript 中的经典继承

在 EcmaScript 6 (ES6) 之前,JavaScript 不支持基于类或经典继承。JavaScript 支持原型继承。原型是属性的对象模板,这些属性被复制到新的对象实例中。您可以将原型视为“主”副本,并将新的对象实例视为主副本的克隆。由于 JavaScript 对象实际上是动态属性包,原型继承将主对象的属性包复制到新对象上。

ES6支持经典继承,但当前浏览器尚未完全支持。然而,经典继承是原型继承的更精炼版本。`inherit.js` 文件通过将基类属性复制到其派生类来模拟经典继承。

/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true, -W055 */

/**
 * Javascript class inheritance.
 *
 * This uses a technique of copying the prototype and properties of the base class
 * to its derive class.
 *
 * @example
 * Object.inherit([derive], base);
 */
(function () {
    "use strict";
    
    /**
     * creates a derive class from the base class.
     *
     * @param {class} clazz     The derive class.
     * @param {class} base      The base class.
     */
    var derive = function(clazz, base) {
        if (clazz.$base)
            return;
        
        // Set the class prototype to the base class.
        if (base) {
            base.prototype.$init = true;
            clazz.prototype = new base();
            delete base.prototype.$init;
            clazz.prototype.constructor = clazz;
            Object.defineProperty(clazz, '$base', {
                get: function() {return base;}
            });
        }
        else {
            Object.defineProperty(clazz, '$base', {
                get: function() {return Object;}
            });
        }
    };

    /**
     * inherit function attached to all objects
     *
     * @param {class} clazz     The derive class.
     * @param {class} base      The base class.
     *
     * @example
     * Object.inherit([derive], base);
     */
    if (!Object.hasOwnProperty('inherit')) {
        Object.defineProperty(Object.prototype, 'inherit', {
            value: function(clazz, base) {
                
                // Derive new class from the base class.
                derive(clazz, base);

                // Set up the instance.
                for (var property in clazz.prototype) { 
                    if (property !== '$init') {
                        Object.defineProperty(this, property,
                            Object.getOwnPropertyDescriptor(clazz.prototype, property));
                    } else {
                        delete clazz.prototype[property];
                    }
                }
                this.$base = (base) ? clazz.prototype : {};
            }
        });
    }
}());
图6:inherit.js

要在 JavaScript 中执行经典继承,`inherit` 方法从基类创建派生类,并递归初始化类层次结构。如果未向 `inherit` 方法提供基类,则假定 `Object` 为基类。下图演示了 `Animal` 类层次结构的定义以及封装、继承和多态的面向对象特性。

/*jshint undef:true, eqeqeq:true, laxbreak:true */
/*global console                                */
function Animal() {
    this.inherit(Animal); 
    
    this.speak = function() {
        console.log("An animal speaks."); 
    };
}

function Dog() {
    this.inherit(Dog, Animal); 
    
    this.speak = function() {
        return "bark!"; 
    };
}

function Cat() {
    this.inherit(Cat, Animal); 
    
    this.speak = function() {
        return "meow!"; 
    };
}

var dog = new Dog();
var cat = new Cat();
console.log(dog.speak()); /* outputs 'bark!' */
console.log(cat.speak()); /* outputs 'meow!' */
图7:使用inherit.js的动物类层次结构

了解面向对象编程后,下一步是识别用于构建 TodoMVC 应用程序的类层次结构。

TodoMVC 类层次结构

下面的类层次结构图显示了实现应用程序所用架构模式的类。此图包括可重用的上述 MVP 类:`Model`、`View` 和 `Controller`。尽管其名称如此,此应用程序的 `Controller` 类提供了表示器的基本功能。`Controller` 还充当 `Model` 和 `View` 之间的中介,以强制它们之间的分离。

特定于 TodoMVC 应用程序操作的类是 `TodoView`、`TodoModel`、`TodoTemplate` 和 `Todos`。`Todos` 类协调 `TodoModel` 和 `TodoView` 之间的交互。`TodoModel` 继承自包含 `Storage` 类的 `Model` 类,该类封装了浏览器的本地存储。`Template` 类为处理模板提供了基本实现,而 `Map` 将键值关联与顺序访问结合起来。

图8:TodoMVC UML,包含类层次结构和组合。

事件驱动设计

层次结构的顶部是 `Dispatcher` 类。`Dispatcher` 提供架构的事件管理。事件是一种发生,与处理程序松散耦合,当通过消息协议发出信号时执行操作。

设想事件和消息协议的最佳方式是将其视为实际的函数调用。与事件不同,函数调用是消息协议和处理程序之间紧密耦合的事件。例如,函数调用 `doSomething()` 紧密绑定了事件(`doSomething` 调用)、消息协议(函数名 `doSomething`)和处理程序(`doSomething` 函数的实现)。

在事件编程中,事件和消息协议是松散耦合的。名为“doSomething”的事件向“Dispatcher”发送一条消息,后者解释该消息,然后*间接*调用“doSomething”处理程序。使用事件驱动方法,doSomething的调用被解耦并分为三个独立阶段,如下所示。

事件阶段

事件驱动调用

函数调用

发生

事件的调用。

事件的触发。

示例
dispatch.trigger('doSomething', eventArgs);

方法的调用。

示例
object.doSomething();

消息协议

描述方法和传递给方法的参数。

 

 

事件名称和事件参数。

示例
dispatch.trigger('doSomething', eventArgs);

消息协议是事件名称 `doSomething`,包括传递给附加到 `doSomething` 事件名称的实现的参数。

方法签名。

示例
object.doSomething();

方法签名表示 `doSomething` 方法的消息协议。

实现

附加到事件的处理程序。

 

 

 

 

 

 

 

 

运行时(后期绑定)附加到事件名称的事件处理程序。事件处理程序在事件触发后执行。

示例
Class myObj {
  void doSomething() {
    console.log('做了些什么');
  }
}

var obj = new myObj();
dispatch.attach('doSomething', obj.doSomething);

`myObj.doSomething` 方法附加到 `doSomething` 事件名称。`doSomething` 方法在名为 `'doSomething'` 的事件触发时执行。

“处理程序”(方法)在方法调用(事件发生)时立即执行(早期绑定)。

示例
var obj = new myObj();
obj.doSomething();

 

`doSomething` 方法在调用时(事件发生)立即执行。

 

 

 

图9:事件的阶段。

标准函数调用也称为“早期绑定”,因为消息协议和处理程序的绑定在运行时之前发生。事件驱动调用称为“后期绑定”,因为消息协议在运行时绑定到函数实现。

通常在C++、Java和C#等静态语言中,首选的绑定方法是早期绑定。这允许编译器通过它们的签名验证函数调用。自然,早期绑定函数提供最佳性能。通过后期绑定,处理程序在运行时附加到事件。后期绑定会影响性能,但由于方法调用与其实现的解耦,因此提供了更大的灵活性。

调度器事件管理

`Dispatcher` 类是 `Model`、`View` 和 `Controller` 的基类。`Dispatcher` 使这些类能够以松散耦合的方式进行交互,尤其是 `View` 和 `Controller` 之间的松散耦合。`View` 响应用户事件,处理用户事件,然后向 `Controller` 发送消息。`View` 不了解 `Controller`,也不依赖于 `Controller`。

图10:调度器类。

`Dispatcher` 基于传入和传出消息的分离思想。对于传入消息,客户端请求 `Dispatcher` 执行服务。客户端请求作为*命令*进行管理。

图11:调度器分离了传入(命令)和传出(事件)消息的概念。

命令代表客户端发送给服务提供商的传入消息,以执行服务。命令表示客户端了解服务提供商。另一方面,事件代表触发事件订阅者的传出消息。这意味着服务提供商不知道事件的目标。在应用程序中,`Todos` 控制器向 `TodoView` 发出命令,以执行服务。在 `TodoView` 捕获并处理用户事件后,向 `Todos` 控制器发布事件,从而发生传出消息。由于 `TodoView` 发布事件,它不知道 `Todos` 控制器,并且 `TodoView` *独立于* `Todos` 控制器运行。

在像 JavaScript 这样的脚本语言中,后期绑定很常见,它减少了关键组件的耦合和*依赖*。为了保持这种松耦合,`Dispatcher` 使用 `EventBus` 类来执行事件发生、事件消息传递和事件处理的管理工作,采用发布者-订阅者模式。下图显示了 `EventBus` 的内部结构。

图12:事件总线的内部结构。

`EventBus` 由发布者和订阅者组成。`Publisher` 类代表单个事件消息,并管理该特定消息的订阅者。`Subscriber` 类代表事件处理程序。因此,单个事件发生会被“发布”给许多订阅者。

委托和订阅者类

`Subscriber` 类是一个绑定函数包装器。在 JavaScript 支持 `bind` 函数之前,绑定函数需要显式闭包定义。绑定函数是具有预设作用域对象的函数。如果函数的作用域对象绑定到 `this` 对象,那么该函数本质上类似于 C# 委托。

`Subscriber` 类使用了下图所示的 `Delegate` 类。此外,`Delegate` 通过其作用域对象提供了额外的验证级别。在取消订阅期间,`Publisher` 通过其作用域对象和回调对象验证 `Subscriber`。

/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/* globals console */
var Delegate = (function () {
    "use strict";

    /**
     * Creates a delegate.
     *
     * A delegate is a callback function when invoked its this variable is set to the
     * predefined scope object.
     *
     * NOTE: This class was written years before bind was natively supported by Javascript
     * functions.  However this class remains useful as bind doesn't support the ability
     * to inspect the scope object bound to the callback.
     *
     * @param {object}     scope       object used to set the callback's scope.
     * @param {function}   callback    the callback function.
     *
     * @returns {function} Delegate invocation function.
     *
     * @example
     * Delegate.create(this, function([arg1[, arg2[, ...]]]) {
     *     ...
     * });
     */
    var create = function(scope, callback) {

        function Delegate(obj, func) {
            var noop = function() {},
                self = obj,
                method = func;

            this.invoke = function () {
                if (self && method) {
                    return (arguments) ? method.apply(self, arguments) : method.apply(self);
                } else {
                    return noop;
                }
            };

            this.invoke.scope = {
                get scope() {
                    return self;
                }
            };

            this.invoke.callback = {
                get callback() {
                    return method;
                }
            };
            
            this.invoke.equals = function (delegate) {
                return this.scope === delegate.scope && this.callback === delegate.callback;
            };
        }

        return new Delegate(scope, callback).invoke;
    };

    return {

        // methods.
        create: create
    };

}());
图13:委托类。

数据管理

TodoMVC 通过浏览器的 `localStorage` 对象持久化数据,它提供了非常易于使用的键值数据访问。这个版本的 TodoMVC 使用 `localStorage` 的一个键值条目来存储待办事项列表。待办事项列表的条目作为单个 JSON 字符串持久化。选择此策略是为了减少对 `localStorage` 的访问,并使持久化更有趣。为了在内存中管理列表项,`Storage` 类使用了 `Map` 类。

`Map` 是一个将关联的键值与顺序访问相结合的集合。因此,集合中的项目只能通过键或索引访问。`Storage` 类使用 `Map` 通过其唯一键访问待办事项,并保留待办事项在列表中的条目顺序。

关注点

本文最初构思为一篇独立的文章,因为要恰当地传达无框架方法,需要深入的解释。由于篇幅较长,本文不得不分为三部分。您刚刚阅读的第一部分提供了无框架的背景、动机和架构方法。第二部分将继续使用第一部分中提出的概念,将TodoMVC实现为无框架应用程序,最后在第三部分,通过一个可运行的无框架应用程序,驳斥了反对无框架方法的论点。

历史

2016年6月11日 解释无框架的背景、动机和架构方法。
2016年6月18日 修复表格格式错误。

© . All rights reserved.