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






4.57/5 (5投票s)
一种用于分离网页中关注点的设计模式。在此,页面的每个部分都被视为一个本质上是响应式的组件,并提供了一种与页面中其他组件进行交互的统一方式。
引言
在处理各种 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 的每个功能部分视为一个组件,它们共同构成一个页面。
当你这样可视化你的页面时,你就能清楚地知道你需要多少个模块?它们的角色是什么?它们如何相互交互?用户如何与它们交互?你的服务如何处理它?
在继续之前,有几点需要注意
- 每个组件都被分配了一项职责,并且它们只服务于该职责。
- 每个组件必须遵循一组规则。
所以,为了继续,让我们在我们的上下文中定义一个组件
引用组件是系统中一个独立的函数单元,代表页面中的特定部分。
并为组件设定规则
- 每个组件都可以拥有状态。
- 只有组件拥有更改其状态的权利。
- 发生在组件上的任何交互都应该是一致的。也就是说,无论交互是通过用户还是代码发生的,组件的行为都应该相同。
- 组件的职责是保护其状态安全,通过公开有意义的方法、属性和事件(注意:不幸的是,我们无法在代码级别强制执行约束)。
- 如果一个组件想要通知其状态的变化,它应该通过触发一个事件来实现,而对该事件感兴趣的组件应该订阅它(观察者模式)。
- 组件中的每个方法都应该只有一项职责(单一职责原则)。
- UI 控件的事件处理程序不应该包含逻辑,事件的意图应该通过调用自身或其他组件的适当方法来满足。
组件可以分为 2 部分
- 系统/框架组件
- 业务组件
顾名思义,业务组件是服务于特定业务目标的组件,而框架组件是可重用的技术组件。
创建组件
我们借助 JavaScript 模块模式创建组件。所以,在我们的上下文中,技术上来说:
引用一个遵守定义的规则以实现功能目标的 JavaScript 模块就是一个组件。
您可能知道,有多种方法可以创建 JavaScript 模块。如果您还不了解,请阅读这篇文章。
嘿……等一下!为什么是模块模式而不是对象?原型对象模型最适合我,组件听起来更像是一个对象而不是一个模块!事实上,我听说组件是一组协同工作的对象。
嗯,使用 JavaScript 的原型对象模型可以实现同样的事情,但我不太推荐,除非它是单例。如果您想知道为什么,那是因为对象的状态。通常,组件确实包含一些标记作为其状态,并且它很容易与多个实例共享,从而导致副作用。
创建框架组件
假设您有一个网站,它需要根据各种情况显示一些通知,例如警告、错误和成功消息。由于通知是在各种页面中都需要的,因此我们将它视为一个框架组件。
所以在我们开始实现之前,让我们先确定它的行为。
- 它应该有一个方法来显示带有给定消息的通知。
- 它应该有一个隐藏的方法。
- 它应该有一个 UI 控件,用户可以通过该控件触发隐藏。
- 它应该公开一个 `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 文件。
让我们回顾一下我们是否正确遵循了所有规则
- 每个组件都可以拥有状态。
是的,我们做到了(标记) - 只有组件拥有更改其状态的权利。
有点是。技术上,脚本的任何其他部分都可以访问此标记,但我们不应该这样做!- 不幸的是,我们无法强制执行 :( - 发生在组件上的任何交互都应该是一致的。也就是说,无论交互是通过用户还是代码发生的,组件的行为都应该相同。
是的,您可以通过单击关闭按钮或从控制台调用 hide 方法来隐藏通知,行为相同。对于其他方法也是如此。 - 组件的职责是保护其状态安全,通过公开有意义的方法、属性和事件(注意:不幸的是,我们无法在代码级别强制执行约束)。
这与第 2 点有点关系,是的,我们通过一些有意义的方法和事件公开了它们,其他代码可以通过它们与之交互。 - 如果一个组件想要通知其状态的变化,它应该通过触发一个事件来实现,而对该事件感兴趣的组件应该订阅它(观察者模式)。
是的,我们通过 onOpen 和 onClose 事件(可选)实现了这一点。
注意:在我们目前的实现中,只有一个用户可以订阅它。如果需要,我们可以使用数组支持多个订阅。 - 组件中的每个方法都应该只有一项职责(单一职责原则)。
是的,通知中的每个方法都只有一项职责。 - UI 控件的事件处理程序不应该包含逻辑,事件的意图应该通过调用自身或其他组件的适当方法来满足。
是的,我们有一个点击事件,它不包含任何逻辑,而是依赖于 hide 方法,从而满足了第 3 点。
恭喜!您已成功遵循了规则,因此您实现的模块是一个组件。:)
后续部分候选
- 创建业务组件
- 处理后端服务
- 处理一些复杂的场景,其中难以识别组件。
关注点
定义 UI 部分的行为并使用控制台与它们交互是一种真正的乐趣!这种方法给我们带来了很多好处。由于我们试图通过定义其行为来使 UI 的某个特定部分成为一个完整的函数,因此可以清楚地了解该部分的可能状态,并且创建错误的可能性非常少。即使出现错误,您也能确切地知道在哪里查找。修改 UI 部分的副作用非常小。
一开始,您可能需要一些时间来理解这一点,但一旦您做到了,它就会自然而然地发生。您可以专注于客户想要什么。
需要注意的一点是,如果您在一个团队中工作,每个团队成员都应该了解这些规则,并且他们必须遵循它们。正如我在文章中多次重申的那样,有些规则无法通过任何工具强制执行。这应该是团队成员之间的相互理解。
这对我来说仍然是一个持续学习的过程。您的反馈、意见和建议非常受欢迎。
历史
- 2016 年 2 月 24 日 - 初始版本