11月阅读周·编写可测试的JavaScript代码:基于事件的架构之事件集线器篇

举报
叶一一 发表于 2024/11/21 10:39:44 2024/11/21
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》

当前阅读周书籍编写可测试的JavaScript代码

事件集线器

事件背后的思想很简单:将方法注册到事件中心,指定其能够处理的某些事件。方法利用集线器独立的中央处理器,负责事件请求,并等待响应。方法既能够注册事件,也能抛出事件。这些方法通过回调接收异步响应。应用程序的类或方法只需要一个事件集线器引用即可。所有通信都通过事件集线器进行处理。程序代码不管是在浏览器上运行,还是在服务器上运行,都可以访问该集线器。

典型的Web应用程序是和Web服务器紧密耦合在一起的,如下图所示。

01.jpg

浏览器和客户端是用户的,连接到提供服务和模块的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服务器不再是程序的中心,而是被降级为一个与其他链接模块类似的服务模块。

02.jpg

这种方式是公共耦合吗?公共耦合是耦合中第二常见的耦合形式,在该耦合中,多个对象共享一个公共的全局变量。不管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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。