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






4.30/5 (7投票s)
jModulizer - 如何模块化和解耦你的 JavaScript 代码。
介绍
jModulizer 是一个为现有项目开发的框架,它有助于开发可维护的 JavaScript 代码。在本文中,我将分享一些 JavaScript 中的模式实现以及如何使用这些模式解决一些问题。此外,你还可以学习“如何开发自己的 JavaScript 库”。
本文不适合 JavaScript 初学者。本文面向希望提升 JavaScript 模式知识的专业人士。了解 JavaScript 基础对于理解本文非常重要。
我开发这个框架是为了应对当今 Web 应用程序面临的问题。开发者遇到的一些常见问题如下:
- 由于代码行数过多而难以维护。
- 结构不佳
- 过多的全局变量
- 高度依赖 jQuery、Kendo 等第三方库。
- 性能问题
- 糟糕的依赖管理使得单元测试困难
- 可重用性问题
- 错误处理问题
- 并行加载问题
JavaScript 目前已成为主流的 Web 开发语言。即使是小型项目,也可能包含 1500 – 3000 行代码。
JavaScript 仍然不是开发者中最受欢迎的语言。因此,不同的开发者使用不同的编码风格,这可能导致代码维护问题。
全局变量不适用于 Web 应用程序。当应用程序中的全局变量和函数的数量增加时,命名冲突的可能性会增加,这可能会导致覆盖已声明的函数或变量。依赖于全局变量的函数与环境紧密耦合。如果环境发生变化,函数就可能意外破坏变量。最容易维护的代码是其中所有变量都在本地定义的代码。
当前环境中使用了大量的第三方库。因此,必须找到一种有效的方式将任何其他库或插件集成到应用程序中。如果这些库函数被到处使用,就无法轻松进行替换。
对于大型应用程序的开发而言,性能是一个非常重要的方面。JavaScript 性能的不足会破坏应用程序的可用性。
当一个模块有许多依赖项时,在不包含其他依赖项的情况下,无法单独测试该模块。
可重用性是任何编程语言的重要特性,但在很多 JavaScript 代码中并未得到很好的支持。
这是一个重要领域,因为如果错误没有得到妥善处理,用户将遇到奇怪的行为。
通常,script 标签会阻塞所有其他内容的下载。如果需要加载许多脚本,其他内容将被阻塞,直到所有脚本都下载完成。
jModulizer 框架是基于以下设计模式开发的。
- 沙箱模式
- 模块模式
- 发布/订阅模式
- 外观模式
除了上面提到的模块模式和沙箱模式,其他都是众所周知的设计模式。在深入了解沙箱模式之前,有必要引起大家对某些设计模式的关注。
命名空间模式
命名空间模式减少了我们程序中全局变量的使用,同时有助于避免命名冲突或过多的名称前缀。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);
此模式本质上只是一个函数表达式(命名或匿名),在其创建后立即执行。
该模式包含以下部分
- 你使用函数表达式定义一个函数。(函数声明不起作用。)
- 在末尾添加一对括号,这将导致函数立即执行。
- 将整个函数包装在一对括号中(仅当你不将函数分配给变量时才需要)。
以下替代语法也很常见(请注意右括号的位置)
(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
的构造函数会检查第一个参数,并通过“b
”对象注入这些依赖项。ox
jModulizer 框架概述
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,你就必须构建自己的通信机制。
- 为开发人员提供了遵循编程指南的方向,最终的代码将是整洁干净的代码。
- 有一个自动错误处理机制,如果开发人员未处理错误,框架会自动处理。(这是一个可预配置的功能,因此仅在生产版本中启用)。
此外,你还可以使用代理、享元等设计模式来提高性能。
上述文章仅包含框架功能及其架构的概述。
我认为仍有一些领域和最佳实践需要进一步开发和改进。然而,该框架已经过实际使用和测试,并证实了其成功运行。