Vanilla JavaScript中的MVC设计模式

在好莱坞标志前拥有笔记本电脑的开发人员-MVC设计模式

设计模式通常被合并到流行的框架中。 例如, 模型-视图-控制器(MVC)设计模式是一种普遍存在的模式。 在JavaScript中,很难将框架与设计模式脱钩。 通常,特定的框架会附带自己对此设计模式的解释。 框架附带意见,每个框架都会迫使您以某种方式进行思考。

现代框架规定了MVC模式的具体实现是什么样的。 当所有解释都不同时,这会造成混乱,从而增加噪音和混乱。 当任何代码库采用多个框架时,都会造成令人沮丧的混乱。 我心中的问题是,有没有更好的方法?

MVC模式适用于客户端框架,但是现代框架正在发生变化。 今天的现代是随着时间的流逝而消逝的。 在这种情况下,我想探索替代方法,并了解一些纪律性要求我们去哪儿。

MVC模式本身可以追溯到几十年前。 这使之成为投资编程技能的不错的设计模式。MVC模式是可以独立存在的设计模式。 问题是,这可以带我们走多远?

等等,这是另一个框架吗?

首先,我想消除这个普遍的神话:设计模式不是框架。 设计模式是解决代码问题的一种规范方法。 有一定水平的技能,必须由程序员负责。 设计模式将关注点分开并提倡简洁的代码。

框架是不同的,因为它不必遵守任何设计模式。 从模式中分辨框架的一种方法是寻找好莱坞原则。 好莱坞的原则是:“不要打电话给我们,我们会打电话给您。” 任何时候只要有依赖项指示何时使用它,它就是一个框架。 框架与好莱坞很像,因为您没有就该做什么或如何做的发言权。 实际上,开发人员就像演员一样,因为他们在被要求行动时遵循脚本。

有充分的理由避免使用客户端框架:

  • 框架增加了解决方案的复杂性和风险
  • 您遇到依赖项锁定,从而导致代码无法维护
  • 随着新的流行框架的出现,很难重写现有的遗留代码

MVC模式

MVC设计模式是从1970年代的Xerox Smalltalk研究项目发展到80年代的。 这种模式经受了前端图形用户界面的时间考验。 这种模式来自桌面应用程序,但事实证明也对Web应用程序有效。

关键在于,MVC设计模式是关注点的清晰划分。 这个想法是使解决方案易于理解和吸引人。 任何希望进行特定更改的程序员都可以轻松找到合适的位置。

企鹅演示

企鹅! 可爱而可爱,地球上一些最毛茸茸的生物。 如此可爱,实际上,有17种不同的企鹅并不都生活在南极洲。

是时候演示企鹅了! 我将在一个页面上显示一个甲板,其中显示了几种物种。 为此,我想使用MVC设计模式和一些技巧。 我将使用极限编程方法来解决单元测试中的问题,而且不要胡扯。 最后,您应该能够浏览几个企鹅,每个企鹅都有自己的数据和个人资料图片。

在本示例的最后,您应该已经学到足够的知识,可以在纯JavaScript中使用MVC设计模式。 模式本身是超级可测试的,因此期望良好的单元测试。

由于跨浏览器兼容性的原因,我将坚持使用ES5进行此演示。 在这种常年性的设计模式中使用经过验证的语言功能是很有意义的。

你准备好了吗? 让我们找出答案。

骨架

该演示将包括三个主要部分:控制器,视图和模型。 每个问题都有其自己的关注点和需要解决的问题。

外观如下所示:

企鹅视觉演示

PenguinController处理事件,并且是视图和模型之间的中介。 它可以弄清楚用户执行某项操作(例如,单击按钮或按下键)时会发生什么。 客户端特定的逻辑可以进入控制器。 在发生很多事情的更大的系统中,您可以将其分解为模块。 控制器是事件的入口点,并且是视图和数据之间的唯一中介。

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中,这意味着一次只关注一个功能。

作为开发人员,很容易相信您是有逻辑的,并且不会情绪激动。 事实是,一次您被太多问题困扰,感到沮丧。 这是我们所有人都必须应对的正常人的回应。 事实是,挫败感会对代码质量产生负面影响。 当这种感觉抓住了您并支配您的工作时,它不再与逻辑有关。 随着解决方案承担更多的风险和复杂的依赖关系,这可能使人丧气。

我喜欢的只是关注一个问题。 一次解决一个问题并获得积极的反馈。 这样,您就可以呆在区域中,高效而无聊。

本文由Vildan Softic进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

From: https://www.sitepoint.com/mvc-design-pattern-javascript/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值