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

网页作为组件集 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (5投票s)

2016年2月24日

CPOL

10分钟阅读

viewsIcon

13226

downloadIcon

150

一种用于分离网页中关注点的设计模式。在此,页面的每个部分都被视为一个本质上是响应式的组件,并提供了一种与页面中其他组件进行交互的统一方式。

引言

在处理各种 Web 项目时,从小型网站到大型企业应用程序,由于前端代码结构不佳,我们经常面临 UI 维护的挑战。在编程世界中,有足够的指南来保持代码的整洁、结构化和模块化。然而,在前端 Web 开发方面,您不会看到大多数这些原则得到遵循。

通常,在典型的服务器端 MVC 架构中,视图是分离的,但脚本不是。在应用程序中可以看到的最大分离是,每个页面只有一个脚本文件

一般而言,网页是多个小型/部分视图(标记)的组合,并且会有一个脚本文件用于操作(技术上,单个页面可以有多个脚本文件,但不推荐 - 如果您不确定原因,请告诉我 - 谷歌也可以帮助您 :))。这个脚本文件可以访问整个页面,问题就出在这里!- 由于我们的脚本文件对其可以访问的页面内容没有限制,我们通常会编写命令式代码来处理页面的各个部分。

那么,这种方法有什么问题?- 嗯,这种方法的真正问题在于管理 UI 的某个部分或整个页面的状态。由于脚本文件中的任何元素都可以被修改,因此很可能会在多个地方重复相同的操作,而原本统一的逻辑则分散在整个文件中。

解决这个问题的方案显然是模块化,并且对每个模块可以访问页面的内容进行一些限制;本文将重点介绍如何在不使用任何第三方 UI 框架(除了我们用于 DOM 操作的 jQuery)的情况下实现这一点。

背景

[本部分包含我如何得出此结论以及该主题的一些历史,如果您愿意,可以跳过。]

近年来,前端 Web 开发发生了巨大的变化,涌现了许多 UI 框架,如 knockout、angular、react、backbone 等等,每个框架都有自己的一套代码结构规则或指南。在这些库/框架中,您会注意到的共同点是模块化。嗯,我不是在谈论库本身有多么模块化;相反,我关注的是它如何帮助/迫使我们(在一定程度上)保持代码的模块化。

尽管这些库有助于我们的代码模块化,但仍然会存在一些漏洞。人们会再次寻找保持代码整洁和有组织的指南,这在各自的社区中都有。例如,如果我使用 angularjs,我倾向于遵循这些指南;类似地,当我(与 ASP.NET MVC 框架一起)使用 knockout 时,我当时是这样组织我的代码

当我深入研究 JavaScript 时,我开始意识到,在开发策略方面,我们并没有真正遵循一些更根本的东西。我们都知道这些事情,并且非常清楚,但当涉及到 JavaScript 时,我们却忘记了应用它们。也许是因为 JavaScript 的怪异特性(实际上不是)让人们多年来都讨厌它,并且在别无选择的情况下使用它!或者也许是因为当时 JavaScript 中很少有用于应用程序开发的 बढ़त,它所做的只是验证。无论如何,现在 JavaScript 已经成为主流(我相信它当之无愧!),但应用程序开发仍然缺乏一些标准。

吸引我最多的是AMD。开发可重用 JavaScript 库的人经常使用它,但大多数应用程序开发者并不使用。此外,对于模块与某些 UI 部分耦合的应用程序开发者来说,可用的指南并不多。然而,现在一年过去了,我为自己制定了一些规则,并将其应用到我们的团队中。相信我,UI 生产力翻了一番!我想与世界分享这个想法。:)

走向组件化

在这里,我们将 UI 的每个功能部分视为一个组件,它们共同构成一个页面。

当你这样可视化你的页面时,你就能清楚地知道你需要多少个模块?它们的角色是什么?它们如何相互交互?用户如何与它们交互?你的服务如何处理它?

在继续之前,有几点需要注意

  1. 每个组件都被分配了一项职责,并且它们只服务于该职责。
  2. 每个组件必须遵循一组规则。

所以,为了继续,让我们在我们的上下文中定义一个组件

引用

组件是系统中一个独立的函数单元,代表页面中的特定部分。

并为组件设定规则

  1. 每个组件都可以拥有状态
  2. 只有组件拥有更改其状态权利
  3. 发生在组件上的任何交互都应该是一致的。也就是说,无论交互是通过用户还是代码发生的,组件的行为都应该相同。
  4. 组件的职责保护其状态安全,通过公开有意义的方法、属性和事件(注意:不幸的是,我们无法在代码级别强制执行约束)。
  5. 如果一个组件想要通知其状态的变化,它应该通过触发一个事件来实现,而对该事件感兴趣的组件应该订阅它(观察者模式)。
  6. 组件中的每个方法都应该只有一项职责(单一职责原则)。
  7. UI 控件的事件处理程序不应该包含逻辑,事件的意图应该通过调用自身或其他组件的适当方法来满足

组件可以分为 2 部分

  1. 系统/框架组件
  2. 业务组件

顾名思义,业务组件是服务于特定业务目标的组件,而框架组件是可重用的技术组件。

创建组件

我们借助 JavaScript 模块模式创建组件。所以,在我们的上下文中,技术上来说:

引用

一个遵守定义的规则以实现功能目标的 JavaScript 模块就是一个组件。

您可能知道,有多种方法可以创建 JavaScript 模块。如果您还不了解,请阅读这篇文章

嘿……等一下!为什么是模块模式而不是对象?原型对象模型最适合我,组件听起来更像是一个对象而不是一个模块!事实上,我听说组件是一组协同工作的对象。

嗯,使用 JavaScript 的原型对象模型可以实现同样的事情,但我不太推荐,除非它是单例。如果您想知道为什么,那是因为对象的状态。通常,组件确实包含一些标记作为其状态,并且它很容易与多个实例共享,从而导致副作用。

创建框架组件

假设您有一个网站,它需要根据各种情况显示一些通知,例如警告、错误和成功消息。由于通知是在各种页面中都需要的,因此我们将它视为一个框架组件。

所以在我们开始实现之前,让我们先确定它的行为。

  1. 它应该有一个方法来显示带有给定消息的通知。
  2. 它应该有一个隐藏的方法。
  3. 它应该有一个 UI 控件,用户可以通过该控件触发隐藏。
  4. 它应该公开一个 `onOpen` 和 `onClose` 事件,以便调用代码可以根据需要执行进一步的操作。

我们定义的行为基本上是应用程序对特定 UI 部分的要求。然而,第 4 点是一个技术方面,不会出现在需求中。

让我们来实现模块

 var notification = (function () {

  // Keep a self reference for all good reasons :)
  var self = this;

  // Lets have a typed hold of markup identity (not necessarily meant to be id, it can be any selector)
  var notifcationDiv = "#notification";
  var messageSpan = "#msg";
  var close = "#close";

  // A private method to show the given message with success class
  var showSuccess = function (message) {
    $(messageSpan).text(message);
    $(notifcationDiv).addClass('alert-success').show();
  };

  // A private method to show the given message with danger class
  var showError = function (message) {
    $(messageSpan).text(message);
    $(notifcationDiv).addClass('alert-danger').show();
  };

  // A private method to show the given message with warning class
  var showWarning = function (message) {
    $(messageSpan).text(message);
    $(notifcationDiv).addClass('alert-warning').show();
  };

  // A private method to hide the notification
  var hide = function () {
    // trigger onClose event, if registered
    if (this.onClose && typeof this.onClose === 'function') {
      this.onClose();
    }
    // Do the clean-up and hide it
    $(notifcationDiv).removeClass().addClass('alert').hide();
  };

  // A private helper method to show the notification.
  var show = function (action, message) {
    // hide any existing notification
    hide.call({});
    // trigger the onOpen event, if registered
    if (this.onOpen && typeof this.onOpen === 'function') {
      this.onOpen();
    }
    // perform the requested operation
    action(message);
  };

  // Register the handler for click event of the close button
  $(close).click(function () {
    // Call the hiding logic using notification. 
    // This is important, when you call a method using the callbacks
    hide.call(self);
  });

  // Revealing the showSuccess method, which takes help of show method to perform its job.
  self.showSuccess = function (message) {
    show(showSuccess, message);
  };

  // Revealing the showError method, which takes help of show method to perform its job.
  self.showError = function (message) {
    show(showError, message);
  };

  // Revealing the showWarning method, which takes help of show method to perform its job.
  self.showWarning = function (message) {
    show(showWarning, message);
  };

  // Revealing the hide method.
  self.hide = hide;

  // Add support for events.
  self.onOpen = null;
  self.onClose = null;

  //------------------------------------------------------------------------
  // NOTE: Technically it is not required to register the events/callbacks 
  //    but it is good to have as the consumer of your module can inspect it
  //------------------------------------------------------------------------

  // Finally expose yourself!
  return self;

}());

这是它的状态 - 通知标记

<div id="notification" class="alert" 
style="display: none">
    <button id="close" type="button" 
    class="close"><span aria-hidden="true">×</span></button>
    <span id="msg"></span>
</div>

我为您设置了一个 code-pen。您可以在此处与代码进行交互 - http://codepen.io/knaveenbhat/pen/dGxere

在页面底部,有一个名为 `console` 的按钮。单击它,它将打开一个 REPL。您可以使用它与我们的 `notification` 组件进行交互。

所以,在控制台中,让我们为 `onOpen` 事件注册一个处理程序

notification.onOpen = function(){ console.log('open...') }

我们也为 `onClose` 事件注册一个处理程序

notification.onClose = function(){ console.log('close...') }

现在输入这个

notification.showSuccess('my first success notification!')

注意白色区域,它应该显示一个通知!另外,在控制台中,您将看到一条消息 `open...`,这是我们的回调的结果。

尝试使用 `showError`、`showWarning`、`hide`,看看它是否按预期工作。通知还有一个关闭按钮,您可以使用它来隐藏通知。

现在我们的通知框架组件已准备就绪,可以使用了!

对于那些想使用纯浏览器的人,我为他们创建了一个 html 文件。请找到附件。抱歉创建了一个 zip 文件来包含单个文件。CodeProject 不允许我附加 html 文件。

让我们回顾一下我们是否正确遵循了所有规则

  1. 每个组件都可以拥有状态
    是的,我们做到了(标记)
  2. 只有组件拥有更改其状态权利
    有点是。技术上,脚本的任何其他部分都可以访问此标记,但我们不应该这样做!- 不幸的是,我们无法强制执行 :(
  3. 发生在组件上的任何交互都应该是一致的。也就是说,无论交互是通过用户还是代码发生的,组件的行为都应该相同。
    是的,您可以通过单击关闭按钮或从控制台调用 hide 方法来隐藏通知,行为相同。对于其他方法也是如此。
  4. 组件的职责保护其状态安全,通过公开有意义的方法、属性和事件(注意:不幸的是,我们无法在代码级别强制执行约束)。
    这与第 2 点有点关系,是的,我们通过一些有意义的方法和事件公开了它们,其他代码可以通过它们与之交互。
  5. 如果一个组件想要通知其状态的变化,它应该通过触发一个事件来实现,而对该事件感兴趣的组件应该订阅它(观察者模式)。
    是的,我们通过 onOpen 和 onClose 事件(可选)实现了这一点。
    注意:在我们目前的实现中,只有一个用户可以订阅它。如果需要,我们可以使用数组支持多个订阅。
  6. 组件中的每个方法都应该只有一项职责(单一职责原则)。
    是的,通知中的每个方法都只有一项职责。
  7. UI 控件的事件处理程序不应该包含逻辑,事件的意图应该通过调用自身或其他组件的适当方法来满足
    是的,我们有一个点击事件,它不包含任何逻辑,而是依赖于 hide 方法,从而满足了第 3 点。

恭喜!您已成功遵循了规则,因此您实现的模块是一个组件。:)

后续部分候选

  1. 创建业务组件
  2. 处理后端服务
  3. 处理一些复杂的场景,其中难以识别组件。

关注点

定义 UI 部分的行为并使用控制台与它们交互是一种真正的乐趣!这种方法给我们带来了很多好处。由于我们试图通过定义其行为来使 UI 的某个特定部分成为一个完整的函数,因此可以清楚地了解该部分的可能状态,并且创建错误的可能性非常少。即使出现错误,您也能确切地知道在哪里查找。修改 UI 部分的副作用非常小。

一开始,您可能需要一些时间来理解这一点,但一旦您做到了,它就会自然而然地发生。您可以专注于客户想要什么。

需要注意的一点是,如果您在一个团队中工作,每个团队成员都应该了解这些规则,并且他们必须遵循它们。正如我在文章中多次重申的那样,有些规则无法通过任何工具强制执行。这应该是团队成员之间的相互理解。

这对我来说仍然是一个持续学习的过程。您的反馈、意见和建议非常受欢迎。

历史

  • 2016 年 2 月 24 日 - 初始版本
© . All rights reserved.