11月阅读周·编写可测试的JavaScript代码:基于事件的架构之事件集线器篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
事件集线器
事件背后的思想很简单:将方法注册到事件中心,指定其能够处理的某些事件。方法利用集线器独立的中央处理器,负责事件请求,并等待响应。方法既能够注册事件,也能抛出事件。这些方法通过回调接收异步响应。应用程序的类或方法只需要一个事件集线器引用即可。所有通信都通过事件集线器进行处理。程序代码不管是在浏览器上运行,还是在服务器上运行,都可以访问该集线器。
典型的Web应用程序是和Web服务器紧密耦合在一起的,如下图所示。
浏览器和客户端是用户的,连接到提供服务和模块的HTTP服务器。所有外部服务都是和HTTP服务器紧密耦合在一起的,并提供了所需的所有其他服务。此外,所有客户端和模块之间的通信都是通过HTTP服务器完成的,这是不合适的,因为HTTP服务器最好只提供静态内容(原有的意图)。基于事件编程的目标是将HTTP服务器替换为事件集线器作为处理中心。
如下代码所示,我们要做的就是向集线器添加一个引用,以便加入系统。这非常适合多个客户端和后端都在一起交互处理的情况:
eventHub.fire('LOG', {
severity: 'DEBUG',
message: "i'm doing something",
});
与此同时,在服务器上的一些地方(任意地方),都可以使用类似如下代码对LOG事件进行监听并进行处理:
eventHub.listen('LOG', logit);
function logit(what) {
// do something interesting
console.log(what.severity + what.message);
}
事件集线器将两个截然相反的代码片段联接在一起。不管幕后发生了什么,不管客户端身在何处,消息都可以通过事件集线器传递给客户端。下图展示了集线器和所有其连接的模块之间的关系。注意HTTP服务器不再是程序的中心,而是被降级为一个与其他链接模块类似的服务模块。
这种方式是公共耦合吗?公共耦合是耦合中第二常见的耦合形式,在该耦合中,多个对象共享一个公共的全局变量。不管EventHub对象是全局的,还是通过参数传递给每个对象的,这都不是公共耦合。虽然所有的对象都使用了公共的EventHub,但它们没有改变它或通过改变它的状态去共享信息,这是EventHub与其他形式全局对象的一个重要区别。尽管所有的通信都是通过EventHub对象进行的,但在EventHub对象里,它们之间没有共享状态。
使用事件集线器
所以,事件集线器的使用也就成了将各部分加入到集线器中、触发、监听并响应事件的一场游戏了。与方法调用(需要对象的局部实例化、耦合、扇出、局部的mock与stub)不同,事件集线器是抛出一个事件并(如果需要,可能)等待响应。
集线器可以在服务器端的任何地方运行。客户端可以与它连接,并触发及订阅事件。
通常,基于浏览器的客户端触发事件,而服务器端的客户端响应事件,当然,任何客户端都可以触发和响应事件。
该架构发挥了JavaScript函数的优势,鼓励使用最小依赖项的小型耦合代码。鼓励开发人员编写使用最小依赖项的小块代码,使用事件而不是方法调用,可以极大地提高可测试性和可维护性。所有外部模块作为服务进行提供,而不是将其作为依赖进行封装。唯一的函数调用应该是对集线器的调用,或对对象或模块局部方法的调用。
让我们看看一些规范的登录逻辑。如下代码,是一个使用YUI框架开发的非常标准的基于Ajax的登录:
YUI().add(
'login',
function (Y) {
Y.one('#submitButton').on('click', login);
function login(e) {
var username = Y.one('#username').get('value'),
password = Y.one('#password').get('value'),
cfg = {
data: json.stringify({
username: username,
password: password,
}),
method: post,
on: {
complete: function (tid, resp, args) {
if (resp.status === 200) {
var response = json.parse(resp.responseText);
if (response.loginOk) {
userLoggedIn(username);
} else {
failedLogin(username);
}
} else {
networkError(resp);
}
},
},
},
request = y.io('/login', cfg);
}
},
'1.0',
{ requires: ['node', 'io-base'] },
);
34行代码,该登录不是太糟。如下是使用基于事件编程的示例(使用YUI的事件集线器模块):
YUI().add(
'login',
function (Y) {
Y.one('#submitButton').on('click', login);
function login(e) {
var username = Y.one('#username').get('value'),
password = Y.one('#password').get('value');
eventHub.fire(
'login',
{
username: username,
password: password,
},
function (err, resp) {
if (!err) {
if (resp.loginOk) {
userLoggedIn(username);
} else {
failedLogin(username);
}
} else {
networkError(resp);
}
},
);
}
},
'1.0',
{ requires: ['node', 'io-base'] },
);
上述代码只有30行,节约了12%的代码。除了有更少的代码以外,也让测试变得简单了。为了说明这一点,让我们分别对Ajax版本和EventHub版本的代码进行单元测试,并进行比较。以Ajax版本的设置开始:
YUI().use('test', 'console', 'node-event-simulate', 'login', function () {
// Factory for mocking Y.io
var getFakeIO = function (args) {
return function (url, config) {
Y.Assert.areEqual(url, args.url);
Y.Assert.areEqual(config.data, args.data);
Y.Assert.areEqual(config.method, args.method);
Y.Assert.isFunction(config.on, args.on);
config.on.complete(1, args.responseArg);
};
},
realIO = Y.io;
});
上述代码是加载外部依赖模块的标准YUI样板代码,包括将要测试的login模块,还包括一个用于创建模拟Y.io实例的工厂,该实例是YUI对XMLHttpRequest的模拟。该工厂对象将确保正确的值被传递给它,最后,保留一个真正Y.io实例的引用,以便可以利用它恢复其他测试。
总结
基于XMLHttpRequest和Y.io的特性越来越多,所以Ajax请求方面的测试代码也越来越多,像成功或失败这样的回调也需要进行模拟。复杂的用法越多,所需测试的复杂性就越大。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)