JavaScript 是最强大的语言之一。它支持广泛的编程风格和技术,但这种灵活性伴随着危险。当没有遵循最佳实践或设计模式应用不正确时,JavaScript 项目相对容易变得混乱。
本文的目标是演示如何在开发简单的 JavaScript 组件时应用模型-视图-控制器模式。该组件是具有可编辑项目列表的 ListBox(“选择”HTML 标记)控件:用户应该能够选择和删除项目并将新项目添加到列表中。
我希望这篇文章本身对你来说可能是一个很好的阅读。但是如果您考虑运行示例并使用它们会更好。
模型-视图-控制器模式需要在这里进行一些描述。模式的名称由其参与者的名称组成: 模型 - 存储应用程序数据;View - 为客户渲染模型;和控制器 - 通过对客户端的操作做出反应来更新模型。Wikipedia 定义了部分模型-视图-控制器架构,如下所示:
- 模型- 应用程序操作的信息的特定领域表示。模型是领域层的另一个名称。域逻辑为原始数据增加了意义(例如,计算今天是否是用户的生日,或者购物车项目的总数、税金和运费)。
- 视图- 将模型呈现为适合交互的形式,通常是用户界面元素。MVC 经常出现在 Web 应用程序中,其中视图是 HTML 页面和为页面收集动态数据的代码。
- 控制器- 处理和响应事件,通常是用户操作,并调用模型和视图的更改。
因此,让我们以反映该设计模式的各个部分的方式设计组件的主要类。
在实现 MVC 类之前,需要选择通知方法。在 JavaScript 中,没有关于如何实现发布-订阅模式的特殊语言结构或推荐接口。所有实现只使用函数引用。其中最常见的可能是EventEmitter实现,例如可以在 node.js 中找到。此实现的两个主要方法是用于添加事件处理程序的“on”和用于调用指定事件的事件处理程序的“emit”。通过从 EventEmitter 扩展通知或通过添加代理对 EventEmitter 内部实例的调用的方法,可以将通知添加到类中。
class EventEmitter {
constructor() {
this._events = {};
}
on(evt, listener) {
(this._events[evt] || (this._events[evt] = [])).push(listener);
return this;
}
emit(evt, arg) {
(this._events[evt] || []).slice().forEach(lsn => lsn(arg));
}
}
组件的数据只是一个项目列表,其中可以选择和删除一个特定项目。因此,组件的模型非常简单——它由一个数组和一个选定项索引组成;这是:
/**
* The Model. Model stores items and notifies
* observers about changes.
*/
class ListModel extends EventEmitter {
constructor(items) {
super();
this._items = items || [];
this._selectedIndex = -1;
}
getItems() {
return this._items.slice();
}
addItem(item) {
this._items.push(item);
this.emit('itemAdded', item);
}
removeItemAt(index) {
const item = this._items.splice(index, 1)[0];
this.emit('itemRemoved', item);
if (index === this._selectedIndex) {
this.selectedIndex = -1;
}
}
get selectedIndex () {
return this._selectedIndex;
}
set selectedIndex(index) {
const previousIndex = this._selectedIndex;
this._selectedIndex = index;
this.emit('selectedIndexChanged', previousIndex);
}
}
在设计 View 之前,我们需要修复组件的 UI 结构。界面有很多替代方案,但就本文而言,最简单的一种更适合。让我们将项目保留在 Listbox 控件中,并在附近添加两个按钮:“加号”按钮用于添加项目,“减号”按钮用于删除选定项目。ListBox 将为我们提供选择项目和导航的低级机制。View 类与 Controller 类紧密绑定,后者“...处理来自用户界面的输入事件,通常通过注册的处理程序或回调”(来自wikipedia.org)。
以下是 View 和 Controller 类:
/**
* The View. View presents the model and provides
* the UI events. The controller is attached to these
* events to handle the user interaction.
*/
class ListView extends EventEmitter {
constructor(model, elements) {
super();
this._model = model;
this._elements = elements;
// attach model listeners
model.on('itemAdded', () => this.rebuildList())
.on('itemRemoved', () => this.rebuildList());
// attach listeners to HTML controls
elements.list.addEventListener('change',
e => this.emit('listModified', e.target.selectedIndex));
elements.addButton.addEventListener('click',
() => this.emit('addButtonClicked'));
elements.delButton.addEventListener('click',
() => this.emit('delButtonClicked'));
}
show() {
this.rebuildList();
}
rebuildList() {
const list = this._elements.list;
list.options.length = 0;
this._model.getItems().forEach(
item => list.options.add(new Option(item)));
this._model.selectedIndex = -1;
}
}
/**
* The Controller. Controller responds to user actions and
* invokes changes on the model.
*/
class ListController {
constructor(model, view) {
this._model = model;
this._view = view;
view.on('listModified', idx => this.updateSelected(idx));
view.on('addButtonClicked', () => this.addItem());
view.on('delButtonClicked', () => this.delItem());
}
addItem() {
const item = window.prompt('Add item:', '');
if (item) {
this._model.addItem(item);
}
}
delItem() {
const index = this._model.selectedIndex;
if (index !== -1) {
this._model.removeItemAt(index);
}
}
updateSelected(index) {
this._model.selectedIndex = index;
}
}
当然,模型、视图和控制器类应该被实例化:
Your favourite JavaScript technologies:<br>
<select id="list" size="10" style="width: 17em"></select><br>
<button id="plusBtn"> + </button>
<button id="minusBtn"> - </button>
window.addEventListener('load', () => {
const model = new ListModel(['node.js', 'react']),
view = new ListView(model, {
'list' : document.getElementById('list'),
'addButton' : document.getElementById('plusBtn'),
'delButton' : document.getElementById('minusBtn')
}),
controller = new ListController(model, view);
view.show();
});