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

jModulizer - 如何模块化和解耦您的 JavaScript 代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.30/5 (7投票s)

2013年6月3日

CPOL

11分钟阅读

viewsIcon

28683

downloadIcon

178

jModulizer - 如何模块化和解耦你的 JavaScript 代码。

介绍 

jModulizer 是一个为现有项目开发的框架,它有助于开发可维护的 JavaScript 代码。在本文中,我将分享一些 JavaScript 中的模式实现以及如何使用这些模式解决一些问题。此外,你还可以学习“如何开发自己的 JavaScript 库”。

本文不适合 JavaScript 初学者。本文面向希望提升 JavaScript 模式知识的专业人士。了解 JavaScript 基础对于理解本文非常重要。

我开发这个框架是为了应对当今 Web 应用程序面临的问题。开发者遇到的一些常见问题如下:

  1. 由于代码行数过多而难以维护。
  2. JavaScript 目前已成为主流的 Web 开发语言。即使是小型项目,也可能包含 1500 – 3000 行代码。

  3. 结构不佳
  4. JavaScript 仍然不是开发者中最受欢迎的语言。因此,不同的开发者使用不同的编码风格,这可能导致代码维护问题。

  5. 过多的全局变量
  6. 全局变量不适用于 Web 应用程序。当应用程序中的全局变量和函数的数量增加时,命名冲突的可能性会增加,这可能会导致覆盖已声明的函数或变量。依赖于全局变量的函数与环境紧密耦合。如果环境发生变化,函数就可能意外破坏变量。最容易维护的代码是其中所有变量都在本地定义的代码。

  7. 高度依赖 jQuery、Kendo 等第三方库。
  8. 当前环境中使用了大量的第三方库。因此,必须找到一种有效的方式将任何其他库或插件集成到应用程序中。如果这些库函数被到处使用,就无法轻松进行替换。

  9. 性能问题
  10. 对于大型应用程序的开发而言,性能是一个非常重要的方面。JavaScript 性能的不足会破坏应用程序的可用性。

  11. 糟糕的依赖管理使得单元测试困难
  12. 当一个模块有许多依赖项时,在不包含其他依赖项的情况下,无法单独测试该模块。

  13. 可重用性问题
  14. 可重用性是任何编程语言的重要特性,但在很多 JavaScript 代码中并未得到很好的支持。

  15. 错误处理问题
  16. 这是一个重要领域,因为如果错误没有得到妥善处理,用户将遇到奇怪的行为。

  17. 并行加载问题
  18. 通常,script 标签会阻塞所有其他内容的下载。如果需要加载许多脚本,其他内容将被阻塞,直到所有脚本都下载完成。

jModulizer 框架是基于以下设计模式开发的。

  1. 沙箱模式
  2. 模块模式
  3. 发布/订阅模式
  4. 外观模式

除了上面提到的模块模式和沙箱模式,其他都是众所周知的设计模式。在深入了解沙箱模式之前,有必要引起大家对某些设计模式的关注。

命名空间模式

命名空间模式减少了我们程序中全局变量的使用,同时有助于避免命名冲突或过多的名称前缀。JavaScript 没有内置的命名空间语法,但这是一种很容易实现的功能。与其用许多函数、对象和其他变量污染全局作用域,不如为应用程序或库创建一个全局对象,并将所有函数/属性添加到这个全局对象中。

JavaScript 变量和函数可以声明如下

var
url = 'www.myurl.com';

function connect() {}
function process() {}
function disconnect() {}

在这段代码中,有一个全局变量和三个全局函数。这段代码存在一个问题;如果代码出现在两个或多个地方,就会发生命名冲突。通过使用命名空间模式,我们可以解决这个问题。

以下是如何通过使用命名空间重构上述代码:

var jModulizer = jModulizer || {}

jModulizer.conManager = jModulizer.conManager || {};
 
jModulizer.conManager.url = 'www.myurl.com';
jModulizer.conManager.connect = function () { };
jModulizer.conManager.process = function () { };
jModulizer.conManger.disconnect = function () { };

或者可以使用对象字面量声明

    jModulizer.conManager = {
        url: 'www.myurl.com',

    connect: function () { },
    process: function () { },
    disconnect: function () { }
}

上面的代码中只有一个全局变量。以下代码用于访问 connect() 函数;

jModulizer.conManager.connect();

模块模式

模块(定义)

“一个更大的系统中可互换的单个部分,易于重用。”

模块模式被广泛使用,因为它提供了结构,并有助于在代码增长时对其进行组织。与其他语言不同,JavaScript 没有包的特殊语法,但模块模式提供了创建独立解耦的代码块的工具,这些代码块可以被视为功能上的黑盒子,并根据你正在编写的软件(不断变化)的需求进行添加、替换或移除。模块模式是多种 JavaScript 设计模式的组合,例如命名空间、立即执行函数、私有和特权成员。

让我们创建 conManager 模块对象;

    var jModulizer = jModulizer || {};
    jModulizer.conManager = {};

现在,conManager 模块的逻辑可以如下开发

jModulizer.conManager = (function
() {
    var self = this;
    self.url = 'www.myurl.com';
    self.connect = function () { };
    self.process = function () { };
    self.disconnect = function () { };
    return {
        url: self.url,
        self.connect,
        process: self.process,
        disconnect: self.disconnect
    };
} ());

这个 jModulizer.conManager 向公众公开了 URL 属性以及 connect、process 和 disconnect 函数。

注意最后一行中的“());”?这个 conManager 是一个立即执行函数。

立即执行函数

立即执行函数模式是一种语法,允许你在函数定义后立即执行它。

立即执行函数可以识别如下

(function () {
    /* Implementation goes here */
}());

你也可以将参数传递给立即执行函数,可以将 window 和 document 对象作为参数传递给函数,如下所示;

(function (window, document) {
    /* Implementation goes here */
} (window, document);

此模式本质上只是一个函数表达式(命名或匿名),在其创建后立即执行。

该模式包含以下部分

  1. 你使用函数表达式定义一个函数。(函数声明不起作用。)
  2. 在末尾添加一对括号,这将导致函数立即执行。
  3. 将整个函数包装在一对括号中(仅当你不将函数分配给变量时才需要)。

以下替代语法也很常见(请注意右括号的位置)

(function () {
    /* Implementation goes here */
})();

此模式很有用,因为它为你的初始化代码提供了一个作用域沙箱。

沙箱模式

沙箱模式解决了命名空间模式的问题

  • 依赖于一个全局变量作为应用程序的全局变量。

在命名空间模式中,无法在同一页面上运行同一应用程序或库的两个版本,因为它们都需要相同的全局符号名称,例如 jModulizer。

  • 在运行时需要输入和解析的长、点分隔名称,例如。

jModulizer.conManager.url

顾名思义,沙箱模式为模块提供了一个“玩耍”的环境,而不会影响其他模块及其个人沙箱。在沙箱模式中,单个全局变量是一个名为 AppSandbox() 的构造函数。你使用此构造函数创建对象,同时还会传递一个回调函数,该函数将成为你代码的隔离沙箱化环境。

new AppSandbox(function (box) {
     /* module logic goes here */
});

对象框将类似于命名空间示例中的 conManager。它将包含使你的代码工作所需的所有库功能。

AppSandbox() 构造函数可以接受一个额外的配置参数(或多个参数),指定此对象实例所需的模块名称。我们希望代码是模块化的,因此 AppSandbox() 提供的大部分功能将包含在模块中。

new AppSandbox(['creditor', 'debtor'], function (box) {



});

现在,使用“box”对象,可以在 AppSandbox 内访问 creditor 和 debtor 模块。这也被称为依赖注入。在创建 AppSandbox 对象时,Sandbox 的构造函数会检查第一个参数,并通过“box”对象注入这些依赖项。

jModulizer 框架概述

JModulizer 框架是在沙箱模式和模块模式的辅助下开发的。框架架构概述图如下

图 01:jModulizer 架构概述图
  • 区域 A – 调停者模式部分
  • 区域 B – 模块模式
  • 区域 C – 外观模式

根据上述架构,核心处于控制沙箱的位置。沙箱可以控制模块,因为模块只了解沙箱,除了自己的对象之外,不了解任何其他外部对象。

jModulizer 核心实现

这是所有核心对象存在的地方。此核心负责注册/启动/停止/加载模块和错误处理。

jModulizer 是此框架的全局变量,类似于 jQuery 中的“jQuery”。jQuery 有两个全局变量,即“jQuery”和“$”符号。我们通过使用单个全局变量来避免与 jModulizer 的命名冲突。jModulizer 的全局设置配置也在核心内部处理。将配置数据与代码分离是一种良好的编程实践,因为设计良好的应用程序会将关键数据保留在主源代码之外。

jModulizer 是一个单例对象,在加载 jModulizer.core.js 时创建。

(function (window, undefined) {
    var jModulizer = window.jModulizer = window.jModulizer || {};
    jModulizer.core = (function () {
/*jModulizer core functionality goes here*/ 

var self = this;

self.config = {
            DEBUG: false
        };
 
self.setup = function (config) {
            self.config = $.extend(self.config, config);
        };
 
        self.isDebug = function () {
            return self.config.DEBUG
        };
 
self.register = function (moduleId, Creator) {
            jModulizer.moduleData[moduleId] = {
                creator: Creator, instance: null, id: moduleId
            };
 };

self.start = function () {

var args = Array.prototype.slice.call(arguments), moduleId = args[0], 
    configuration = args[1] ? args[1] : null, module = jModulizer.moduleData[moduleId];
module.instance = module.creator(jModulizer.sandbox(self));
if (!self.isDebug())
                     self.errorHandler(module.instance);

module.instance.init(configuration);           
                };
               
                self.stop = function (moduleId) {
    var data = jModulizer.moduleData[moduleId];
    if (data.instance) {
        data.instance.dispose();
        data.instance = null;
    }
};
 
                self.jsLib = jQuery;
 
self.errorHandler = function (object) {
    var name, method;
    for (name in object) {
        method = object[name];
        if (typeof method == "function") {
            object[name] = function (name, method) {
                return function () {
                    try {
                        return method.apply(this, arguments);
                    } catch (ex) {
                        self.displayMsg({ method: name, message: ex.message });
                    }
                };
            } (name, method);
        }
    }
};

return {
                register: self.register,
                start: self.start,
                stop:self.stop,
                jsLib: self.jsLib                     
}  
 })();
window.jModulizer = jModulizer;
jModulizer.moduleData = {};
})(window);

这里,全局错误处理程序是预先配置好的。我们可以使用 jModulizer 全局设置函数进行配置,并仅在生产模式下启用错误处理程序。否则……

在这里,将 window 作为局部变量比全局变量速度稍快。名为 undefined 的参数没有任何值。当你不为参数传递值时,它将被设置为 undefined。因此,在函数块内部,名为 undefined 的参数将具有 undefined 的值。这样做的目的是因为在早期版本的 JavaScript 中,全局标识符 undefined 不是常量,所以你可以给属性 undefined 赋值,使其不再代表 undefined。(请注意,没有值的参数将获得实际的 undefined 值,而不是全局属性 undefined 的当前值。)

jModulizer 调停者

jModulizer 调停者负责模块之间的通信。任何模块都可以发布带有相关数据的消息,其他模块如果订阅了该消息就可以做出响应。

(function (window, undefined) {
    var jModulizer = window.jModulizer = window.jModulizer || {};
    jModulizer.com = function () {
        var handlers = {};
 
        return {
            subscribe: function (msg) {
                var type = msg.type;
                handlers[type] = handlers[type] || [];
                if (!this.subscribed(msg))
                    handlers[type].push({ context: msg.context, callback: msg.callback });
            },
 
            subscribed: function (msg) {
                var subscribers = handlers[msg.type], i;
                for (i = 0; i < subscribers.length; i++) {
                    if (subscribers[i].context.id === msg.context.id)
                        return true;
                }
                return false;
            },
 
            publish: function (msg) {
                if (!msg.target) {
                    msg.target = this;
                }
                var type = msg.type;
                if (handlers[type] instanceof Array) {
                    var msgList = handlers[type];
                    for (var i = 0, len = msgList.length; i < len; i++) {
                        msgList[i].callback.call(msgList[i].context, msg.data);
                    }
                }
            },
 
            remove: function (msg) {
                var type = msg.type, callback = msg.callback, handlersArray = handlers[type];
                if (handlersArray instanceof Array) {
                    for (var i = 0, len = handlersArray.length; i < len; i++) {
                        if (handlersArray[i].callback == callback) {
                            break;
                        }
                    }
                    handlers[type].splice(i, 1);
                }
            },
        };
    } ();
})(window);

这里,消息可以如下单独定义

var jModulizer = window.jModulizer = window.jModulizer || {};
jModulizer.messages = {
    SYS_ERROR: "SYS_ERROR",
    SAVED: "SAVED"   
}

jModulizer 沙箱实现

这包括可以在任何模块中使用的功能。如果你仔细观察上面的核心实现、start 函数和模块启动,你会发现核心对象被传递给了沙箱构造函数。因此,沙箱可以访问核心的 public 属性和函数,并且由于沙箱对象被传递给模块,模块可以访问沙箱的公共属性和函数。

(function (window, undefined) {
var jModulizer = window.jModulizer = window.jModulizer || {};
jModulizer.sandbox = function (core) {
var self = this;
 
self.publish = function (msg) {
            if (msg instanceof Array) {
                for (var i = msg.length - 1; i >= 0; i--) {
                    jModulizer.com.publish(msg[i]);
                }
            }
            else {
                jModulizer.com.publish(msg);
            }
        };
 
        self.subscribe = function (msg) {
            if (msg instanceof Array) {
                for (var i = msg.length - 1; i >= 0; i--) {
                    jModulizer.com.subscribe(msg[i]);
                }
            }
            else {
                jModulizer.com.subscribe(msg);
            }
        };
 
self.$ = function (selector) {
return new self.internalCls(selector);
};
 
self.internalCls = function (selector) {
this.elements = core.jsLib(selector);
};
 
self.internalCls.prototype = {
jModulizerTabs: function (options) {
return this.elements.tabs(options); //de-coupled jqueryui tabs
},
jModulizerGrid: function (options) {
return this.elements.kendoGrid(options); //de-coupled kendo grid
}
};
 
self.$.jModulizerAjax = function (options) {
core.jsLib.ajax(options); //de-coupled jquery ajax
}

            self.extend = function extend(defaults, options) {
return core.jsLib.extend(defaults, options);
};
 
return {
subscribe: self.subscribe,
publish: self.publish,
$: self.$,
extend: self.extend
};
};
})(window);

这里,“jModulizer.com”是 jModulizer 调停者。我们可以如下发布/订阅消息。

发布(广播)

sandbox.publish({ type: jModulizer.messages.SAVED,  data: { creditorId: 100 } });

订阅(监听)

sandbox.subscribe({ type: jModulizer.messages.SAVED, callback: handler, context: this});
function handler(data){     /* Message handling logic goes here */ }

这里,“SAVED”是 jModulizer.messages 中的消息名称。

沙箱还可以用于解耦核心库,如 jQuery、Underscore 等。其优点是模块不知道应用程序中使用了哪些库。在这种情况下,如果后续阶段出现更改核心库(例如,从 jQuery 更改为 Underscore)的需求,则可以在不更改模块的情况下完成,只需对沙箱进行少量更改(编写小型适配器)。

解耦如何工作

沙箱本身有自己的符号“$”。这与 jQuery $ 不同。Sandbox 中的对象 $ 为模块提供核心 JavaScript 库对象。在上面的例子中,jQuery 全局对象由 Sandbox 中的 $ 提供。因此,模块不知道使用了哪个基础库。模块只知道 Sandbox 中的 $。

jModulizer 模块注册

jModulizer 核心具有模块注册功能。它接受两个参数,第一个是模块名,第二个是模块对象。

每个模块必须具有两个 public 方法,init()destroy(),因为 jModulizer 核心需要在开始和停止时使用它们。这两个函数分别作为模块的构造函数和析构函数。

jModulizer.core.register("jModulizer.module.loanCalculator", function (sandbox) {
    var self = this;
 
    self.config = {
        loanAmount: 0,
        period: 0,
        interestRate: 0
    };
 
    self.init = function (options) {
        self.config = sandbox.extend(self.config, options);
        sandbox.$('#loanAmount').val(self.config.loanAmount);
        sandbox.$('#loanPeriod').val(self.config.period);
        sandbox.$('#loanInterestRate').val(self.config.interestRate);
        sandbox.$('#calculate').click(self.calculateLoanInstallment);
    };
 
    self.calculateLoanInstallment = function () {
        self.config.loanAmount = sandbox.$('#loanAmount').val();
        self.config.period = sandbox.$('#loanPeriod').val();
        self.config.interestRate = sandbox.$('#loanInterestRate').val();
 
        var monthlyInterestInstallment = 
            new Number((self.config.loanAmount * self.config.interestRate) / 
			(12 * self.config.period)),
            monthlyCapitalInstallment = new Number(self.config.loanAmount / 
			(12 * self.config.period)),
            monthlyTotalInstallment = new Number
		(monthlyInterestInstallment + monthlyCapitalInstallment);
        sandbox.$("#monthlyInterestInstallment").val(monthlyInterestInstallment);
        sandbox.$("#monthlyCapitalInstallment").val(monthlyCapitalInstallment);
        sandbox.$("#monthlyTotalInstallment").val(monthlyTotalInstallment);
    };
 
    self.dispose = function () {
    };
 
    return {
        init: self.init,
        destroy: self.dispose
    };
});

如你所见,此模块具有沙箱对象访问权限。我们可以在注册后以以下方式启动模块

jModulizer.start("jModulizer.module.loanCalculator ");

jModulizer.start("jModulizer.module.loanCalculator ", { /* Startup arguments */ });

你可以在核心中看到 start() 函数的行为。第一个参数是模块名称,第二个参数是可选的启动参数。例如,可以传递 DOM 元素 ID 或类名作为启动参数。

当模块启动时,其 init() 函数会自动调用。要停止模块,请调用 stop 函数

jModulizer.stop("jModulizer.module.loanCalculator");

这将自动调用模块的 destroy() 函数。模块内的 destroy() 函数可用于刷新对象、取消绑定事件等。

如何使用 QUnit 单元测试框架测试“loanCalculator”模块。此模块可以单独测试,无需任何其他模块依赖。

test("Test loan calculation output", 2, function () {
jModulizer.core.start("jModulizer.module.loanCalculator", {
    loanAmount: 100000,
    period: 2,
    interestRate: 0.10
});
 
var calculateButton = $("#calculate");
calculateButton.trigger('click');
 
var monthlyInterestInstallment = $("#monthlyInterestInstallment").val(),
    monthlyCapitalInstallment = $("#monthlyCapitalInstallment").val(),
    monthlyTotalInstallment = $("#monthlyTotalInstallment").val();

equal(monthlyInterestInstallment, 416.67, "monthly interest installment correctly calculated");
equal(monthlyCapitalInstallment, 4166.67, "monthly capital installment correctly calculated");
equal(monthlyTotalInstallment, 4583.34, "monthly total installment correctly calculated");
});

我们从开发 jModulizer 中学到了什么

  • 可以轻松地将现有 Web 应用程序的旧 JavaScript 代码适配到此框架。
  • 几乎所有 Web 应用程序都包含非独立的类,每个类都与其他类耦合。通过使用这个开发的框架,这些类可以很容易地被制成独立的模块。
  • Web 应用程序的每个模块都可以单独、独立地进行测试。
  • 通过此框架创建的模块可以并行加载,而 script 标签(<script> </script>)会阻塞 HTML 内容的下载。你可以使用 requirejs 或 headJS 库来并行加载这些模块。
  • 此框架内置了调停者,用于模块之间的通信。如果你使用 requirejs 或其他库来模块化 JavaScript,你就必须构建自己的通信机制。
  • 为开发人员提供了遵循编程指南的方向,最终的代码将是整洁干净的代码。
  • 有一个自动错误处理机制,如果开发人员未处理错误,框架会自动处理。(这是一个可预配置的功能,因此仅在生产版本中启用)。

此外,你还可以使用代理、享元等设计模式来提高性能。

上述文章仅包含框架功能及其架构的概述。  

我认为仍有一些领域和最佳实践需要进一步开发和改进。然而,该框架已经过实际使用和测试,并证实了其成功运行。

© . All rights reserved.