vanilla
设计模式通常被合并到流行的框架中。 例如, 模型-视图-控制器(MVC)设计模式是一种普遍存在的模式。 在JavaScript中,很难将框架与设计模式脱钩。 通常,特定的框架会带有自己对此设计模式的解释。 框架附带意见,每个框架都迫使您以某种方式进行思考。
现代框架规定了MVC模式的具体实现是什么样的。 当所有解释都不同时,这会造成混乱,从而增加噪音和混乱。 当任何代码库采用多个框架时,都会造成令人沮丧的混乱。 我心中的问题是,有没有更好的方法?
MVC模式适用于客户端框架,但是现代框架正在发生变化。 今天的现代是随着时间的流逝而消逝的。 在这种情况下,我想探索替代方法,并了解一些纪律性要求。
MVC模式本身可以追溯到几十年前。 这使之成为投资编程技能的良好设计模式。MVC模式是可以独立存在的设计模式。 问题是,这可以带我们走多远?
等等,这是另一个框架吗?
首先,我想消除这个普遍的神话:设计模式不是框架。 设计模式是一种解决代码问题的规范方法。 有一定水平的技能,必须由程序员负责。 设计模式将关注点分开并提倡简洁的代码。
框架是不同的,因为它不必遵守任何设计模式。 从模式中分辨框架的一种方法是寻找好莱坞原则。 好莱坞的原则是:“不要给我们打电话,我们会给您打电话。” 任何时候只要有依赖项指示何时使用它,它就是一个框架。 框架与好莱坞很像,因为您没有就该做什么或如何做的发言权。 实际上,开发人员就像演员,因为他们在被要求采取行动时会遵循脚本。
有充分的理由避免使用客户端框架:
MVC模式
MVC设计模式是从1970年代的Xerox Smalltalk研究项目发展到80年代的。 这种模式经受了前端图形用户界面的时间考验。 该模式来自桌面应用程序,但事实证明也对Web应用程序有效。
关键在于,MVC设计模式是关注点的清晰划分。 目的是使解决方案易于理解和吸引人。 任何希望进行特定更改的程序员都可以轻松找到合适的位置。
企鹅演示
企鹅! 可爱而可爱,地球上一些最毛茸茸的生物。 如此可爱,实际上,有17种不同的企鹅并不都生活在南极洲。
是时候演示企鹅了! 我将在一个页面上显示一个甲板,其中显示了几种物种。 为此,我想使用MVC设计模式和一些技巧。 我将使用极限编程方法来解决单元测试中的问题,而且不要胡扯。 最后,您应该能够浏览一些企鹅,每个企鹅都有自己的数据和个人资料图片。
在本示例的最后,您应该已经学到足够的知识,可以在纯JavaScript中使用MVC设计模式。 模式本身是超级可测试的,因此需要良好的单元测试。
由于跨浏览器兼容性的原因,我将坚持使用ES5进行此演示。 在这种常年性的设计模式中使用经过验证的语言功能很有意义。
你准备好了吗? 让我们找出答案。
骨架
该演示将包括三个主要部分:控制器,视图和模型。 每个问题都有其自己的关注点和需要解决的问题。
外观如下所示:
PenguinController
处理事件,并且是视图和模型之间的中介。 它可以计算出用户执行某项操作(例如,单击按钮或按下键)时会发生什么。 客户端特定的逻辑可以进入控制器。 在发生很多事情的较大系统中,您可以将其分解为模块。 控制器是事件的入口点,并且是视图和数据之间的唯一中介。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
PenguinView
关心DOM。 DOM是用于进行HTML操作的浏览器API。 在MVC中,除了视图之外,没有其他部分关心更改 DOM。 该视图可以附加用户事件,但将事件处理问题留给控制器。 视图的主要指令是更改用户在屏幕上看到的状态。 对于此演示,视图将使用纯JavaScript进行DOM操作。
PenguinModel
关心数据。 在客户端JavaScript中,这表示Ajax。 MVC模式的一个优点是您现在可以在服务器端进行Ajax调用了。 这对不熟悉该解决方案的其他程序员很有吸引力。 此设计模式中的模型仅关心JSON或来自服务器的对象。
一种反模式是违反这种固有的关注点分离。 例如,该模型必须不关心HTML。 该视图一定不在乎Ajax。 控制器必须充当中介者,而不必担心实现细节。
我发现这种模式是开发人员的初衷是善意的,但担心泄漏。 诱人的是将所有内容都变成一个Web组件并以糊糊告终。 重点放在功能和用户面临的问题上。 但是,功能问题与功能问题不同。
在编程中,我喜欢对功能问题进行明确的分离。 每个单独的编程问题都有解决问题的一致方法。 这使您在阅读代码时更加容易理解。 这个想法是编写引人入胜的代码,以便其他人也可以做出积极的贡献。
如果没有可以看到和触摸的真实示例,那么演示就不算什么。 因此,事不宜迟,下面是展示企鹅演示的CodePen:
请参阅CodePen上的SitePoint ( @SitePoint )提供的Pen Pen 企鹅演示 。
聊够了,花些时间编写一些代码。
控制器
视图和模型是控制器使用的两个组件。 控制器在其构造函数中具有完成该工作所需的所有组件:
var PenguinController = function PenguinController(penguinView, penguinModel) {
this.penguinView = penguinView;
this.penguinModel = penguinModel;
};
构造函数使用控件的反转并以这种方式注入模块。 通过此模式,您可以注入满足高级合同的任何组件。 可以将其视为从实现细节中抽象代码的好方法。 这种模式使您能够使用纯JavaScript编写干净的代码。
然后,通过以下方式连接并处理用户事件:
PenguinController.prototype.initialize = function initialize() {
this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this);
};
PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) {
var target = e.currentTarget;
var index = parseInt(target.dataset.penguinIndex, 10);
this.penguinModel.getPenguin(index, this.showPenguin.bind(this));
};
请注意,此事件使用当前目标来获取存储在DOM中的状态。 在这种情况下,DOM会告诉您所有需要了解的有关其当前状态的信息。 DOM的当前状态是用户在浏览器上看到的内容。 您可以将状态数据存储在DOM本身中,只要控制器不更改状态即可。
触发事件后,控制器将获取数据并说明接下来会发生什么。 this.showPenguin()
回调很有趣:
PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) {
var penguinViewModel = {
name: penguinModelData.name,
imageUrl: penguinModelData.imageUrl,
size: penguinModelData.size,
favoriteFood: penguinModelData.favoriteFood
};
penguinViewModel.previousIndex = penguinModelData.index - 1;
penguinViewModel.nextIndex = penguinModelData.index + 1;
if (penguinModelData.index === 0) {
penguinViewModel.previousIndex = penguinModelData.count - 1;
}
if (penguinModelData.index === penguinModelData.count - 1) {
penguinViewModel.nextIndex = 0;
}
this.penguinView.render(penguinViewModel);
};
控制器为每个企鹅计算索引,并告诉视图进行渲染。 它从模型中获取数据,并将其转换为视图可以理解和关心的对象。
这是显示企鹅时幸福道路的单元测试:
var PenguinViewMock = function PenguinViewMock() {
this.calledRenderWith = null;
};
PenguinViewMock.prototype.render = function render(penguinViewModel) {
this.calledRenderWith = penguinViewModel;
};
// Arrange
var penguinViewMock = new PenguinViewMock();
var controller = new PenguinController(penguinViewMock, null);
var penguinModelData = {
name: 'Chinstrap',
imageUrl: 'http://chinstrapl.jpg',
size: '5.0kg (m), 4.8kg (f)',
favoriteFood: 'krill',
index: 2,
count: 5
};
// Act
controller.showPenguin(penguinModelData);
// Assert
assert.strictEqual(penguinViewMock.calledRenderWith.name, 'Chinstrap');
assert.strictEqual(penguinViewMock.calledRenderWith.imageUrl, 'http://chinstrapl.jpg');
assert.strictEqual(penguinViewMock.calledRenderWith.size, '5.0kg (m), 4.8kg (f)');
assert.strictEqual(penguinViewMock.calledRenderWith.favoriteFood, 'krill');
assert.strictEqual(penguinViewMock.calledRenderWith.previousIndex, 1);
assert.strictEqual(penguinViewMock.calledRenderWith.nextIndex, 3);
PenguinViewMock
具有与实际实现相同的约定。 这使得编写单元测试和进行断言成为可能。 该assert
来自Node断言 ,在Chai断言中也可用。 这使您能够编写可以在Node和浏览器上运行的测试。
注意,控制器并不关心实现细节。 它使用视图提供的合同,例如this.render()
。 这是编写干净代码所必需的准则。 控制器可以信任每个组件执行其将要执行的操作。 这增加了透明度,使代码可读。
风景
该视图仅关心DOM元素和连接事件,例如:
var PenguinView = function PenguinView(element) {
this.element = element;
this.onClickGetPenguin = null;
};
当它更改用户看到的状态时,实现如下所示:
PenguinView.prototype.render = function render(viewModel) {
this.element.innerHTML = '<h3>' + viewModel.name + '</h3>' +
'<img class="penguin-image" src="' + viewModel.imageUrl +
'" alt="' + viewModel.name + '" />' +
'<p><b>Size:</b> ' + viewModel.size + '</p>' +
'<p><b>Favorite food:</b> ' + viewModel.favoriteFood + '</p>' +
'<a id="previousPenguin" class="previous button" href="javascript:void(0);"' +
' data-penguin-index="' + viewModel.previousIndex + '">Previous</a> ' +
'<a id="nextPenguin" class="next button" href="javascript:void(0);"' +
' data-penguin-index="' + viewModel.nextIndex + '">Next</a>';
this.previousIndex = viewModel.previousIndex;
this.nextIndex = viewModel.nextIndex;
// Wire up click events, and let the controller handle events
var previousPenguin = this.element.querySelector('#previousPenguin');
previousPenguin.addEventListener('click', this.onClickGetPenguin);
var nextPenguin = this.element.querySelector('#nextPenguin');
nextPenguin.addEventListener('click', this.onClickGetPenguin);
nextPenguin.focus();
};
注意,它的主要关注点是将视图模型数据转换为HTML并更改状态。 第二个是连接单击事件,并让控制器充当入口点。 状态更改后,事件处理程序将附加到DOM。 此技术可以一目了然地处理事件管理。
为了测试这一点,我们可以验证元素是否已更新并更改状态:
var ElementMock = function ElementMock() {
this.innerHTML = null;
};
// Stub functions, so we can pass the test
ElementMock.prototype.querySelector = function querySelector() { };
ElementMock.prototype.addEventListener = function addEventListener() { };
ElementMock.prototype.focus = function focus() { };
// Arrange
var elementMock = new ElementMock();
var view = new PenguinView(elementMock);
var viewModel = {
name: 'Chinstrap',
imageUrl: 'http://chinstrap1.jpg',
size: '5.0kg (m), 4.8kg (f)',
favoriteFood: 'krill',
previousIndex: 1,
nextIndex: 2
};
// Act
view.render(viewModel);
// Assert
assert(elementMock.innerHTML.indexOf(viewModel.name) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.imageUrl) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.size) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.favoriteFood) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.previousIndex) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.nextIndex) > 0);
这解决了所有大问题,状态更改和接线事件。 但是,数据从哪里来?
该模型
在MVC中,所有模型关心的都是Ajax。 例如:
var PenguinModel = function PenguinModel(XMLHttpRequest) {
this.XMLHttpRequest = XMLHttpRequest;
};
注意,模块XMLHttpRequest
被注入到构造函数中。 这是一种让其他程序员知道该模型需要哪些组件的方法。 如果模型需要的不仅仅是简单的Ajax,则可以使用更多模块来发出信号。 另外,通过单元测试,我可以注入与原始模块具有完全相同协定的模拟。
根据索引获取企鹅的时间:
PenguinModel.prototype.getPenguin = function getPenguin(index, fn) {
var oReq = new this.XMLHttpRequest();
oReq.onload = function onLoad(e) {
var ajaxResponse = JSON.parse(e.currentTarget.responseText);
// The index must be an integer type, else this fails
var penguin = ajaxResponse[index];
penguin.index = index;
penguin.count = ajaxResponse.length;
fn(penguin);
};
oReq.open('GET', 'https://codepen.io/beautifulcoder/pen/vmOOLr.js', true);
oReq.send();
};
这指向一个端点并从服务器获取数据。 我们可以通过使用单元测试模拟数据来测试它:
var LIST_OF_PENGUINS = '[{"name":"Emperor","imageUrl":"http://imageUrl",' +
'"size":"36.7kg (m), 28.4kg (f)","favoriteFood":"fish and squid"}]';
var XMLHttpRequestMock = function XMLHttpRequestMock() {
// The system under test must set this, else the test fails
this.onload = null;
};
XMLHttpRequestMock.prototype.open = function open(method, url, async) {
// Internal checks, system under test must have a method and url endpoint
assert(method);
assert(url);
// If Ajax is not async, you’re doing it wrong :-)
assert.strictEqual(async, true);
};
XMLHttpRequestMock.prototype.send = function send() {
// Callback on this object simulates an Ajax request
this.onload({ currentTarget: { responseText: LIST_OF_PENGUINS } });
};
// Arrange
var penguinModel = new PenguinModel(XMLHttpRequestMock);
// Act
penguinModel.getPenguin(0, function onPenguinData(penguinData) {
// Assert
assert.strictEqual(penguinData.name, 'Emperor');
assert(penguinData.imageUrl);
assert.strictEqual(penguinData.size, '36.7kg (m), 28.4kg (f)');
assert.strictEqual(penguinData.favoriteFood, 'fish and squid');
assert.strictEqual(penguinData.index, 0);
assert.strictEqual(penguinData.count, 1);
});
如您所见,该模型仅关心原始数据。 这意味着使用Ajax和JavaScript对象。 如果您对Vanilla JavaScript中的Ajax不清楚,则可以参阅这篇文章,了解更多信息。
单元测试
无论采取任何纪律,重要的是要做必要的工作以获得保证。 MVC设计模式并不决定如何解决问题。 设计模式为您提供了广泛的界限,使您能够编写简洁的代码。 这使您免于依赖的压迫。
对我来说,这意味着每个用例都有完整的单元测试套件。 这些测试提供了有关如何使用代码的指导。 这使它变得开放并吸引任何希望进行特定更改的程序员。
随意查看整个单元测试集 。 我认为它将帮助您了解这种设计模式。 每个测试都针对特定的用例; 认为它是关注点。 单元测试可帮助您单独考虑每个编码问题并解决这一问题。 每个单元测试中,MVC中功能关注点的分离变得栩栩如生。
展望未来
企鹅演示仅提供了一个可行的概念来展示MVC的实用性。 但是,有很多改进可以迭代:
- 添加一个包含所有企鹅列表的屏幕
- 添加键盘事件,以便您可以翻阅企鹅,也可以添加滑动
- SVG图表以可视化数据,选择任何数据点,例如企鹅大小
当然,我的读者请自行决定是否进一步推广此演示。 这些只是一些想法,因此您可以展示此设计模式的强大功能。
结论
我希望您能看到MVC设计模式和一些纪律可以带您去哪里。 良好的设计模式在推广简洁代码的同时不会受到影响。 它可以让您专心解决问题,同时仅解决当前的问题。 它使您成为一个更好,更有效的程序员。
在编程中,其思想是在解决问题的同时保持手头的问题。 编程技术一次充实了一个单一的问题。 在MVC中,这意味着一次只关注一个功能。
作为开发人员,很容易相信您是有逻辑的,并且不会情绪激动。 事实是,一次您被太多问题困扰,感到沮丧。 这是我们所有人都必须应对的正常人类React。 事实是,挫败感会对代码质量产生负面影响。 当这种感觉抓住了您并支配了您的工作时,它就不再与逻辑有关。 随着解决方案承担更多的风险和复杂的依赖关系,这可能使人丧气。
我喜欢的只是关注一个问题。 一次解决一个问题并获得积极的反馈。 这样,您就可以呆在区域中,高效而无聊。
本文由Vildan Softic同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
翻译自: https://www.sitepoint.com/mvc-design-pattern-javascript/
vanilla