JavaScript 最好的部分之一,也可能是最差的。毫无疑问,这是一种在 HTML 文档的头部添加开始和结束脚本标记并在其中抛出一些意大利面条式代码的简单能力。瞧!有用!或者是吗?你问的意大利面条代码是什么?意大利面条代码是一个不讨人喜欢的术语,指的是杂乱无章、控制结构错综复杂、到处都是的代码。几乎不可能维护和调试,通常结构很差,而且容易出错。
那么如何停止编写这种代码呢?在我看来,你只有两个选择。同样,这只是我的意见。第一,您可以使用无数可用的 JavaScript 框架中的任何一个。或者二,您将学习如何使用某种模式或结构编写 JavaScript 代码。MVC、MVP 和 MVVM 是一些常见的模式,可帮助指导开发人员创建抽象和解耦的解决方案。这些模式之间的主要区别归结为数据层、表示层和应用程序逻辑的处理方式。在这篇博文中,您将关注 MVC 或模型-视图-控制器模式。
模型(数据层)- 这是为您的应用存储数据的地方。该模型与视图和控制器分离,并且故意忽略了更广泛的上下文。每当模型发生更改时,它都会使用事件调度程序通知其观察者发生了更改。您将很快了解事件调度程序。在您将要构建的待办事项列表应用程序中,模型将保存任务列表并负责对每个任务对象执行的任何操作。
View(Presentation Layer)——这部分你的App可以访问DOM,负责设置Event Handlers,比如:click、onmouseover、onmouseout等。view还负责HTML的展示。在您的 To Do List App 中,视图将负责向用户显示任务列表。但是,每当用户通过输入字段输入新任务时,视图将使用事件调度程序通知控制器,然后控制器将更新模型。这允许视图与模型的快速分离。
控制器(应用程序逻辑)——控制器是模型和视图之间的粘合剂。控制器处理并响应模型或视图引发的事件。它在用户操作视图时更新模型,也可用于在模型更改时更新视图。在本教程中,您将通过调度事件直接从模型更新视图。但是,任何一种方式都是完全可以接受的。在你的 To Do list App 中,当用户点击添加任务按钮时,点击转发到控制器,控制器通过添加任务来修改模型。任何其他决策或逻辑也可以在这里执行,例如:使用 HTML 本地存储保存数据,异步保存到数据库/服务器等等。
Event Dispatcher - Event Dispatcher 是一个对象,允许您将无限数量的函数/方法附加到它。当您最终调用该 Event 对象的 notify 方法时,您附加到该 Event 的每个方法都将运行。您将在下面的源代码中看到这种情况发生了很多。
现在您已经基本了解了为什么应该使用 JavaScript 设计模式以及 MVC 架构的全部内容,现在您可以开始构建应用程序了。以下是本教程如何工作的快速概述。首先,我将向您展示代码。这将使您有机会检查并完成它。其次,我将详细介绍以下代码库使用的一些核心概念,并尝试阐明任何可能令人困惑的潜在模糊或灰色区域。最后,我将向您展示最终组件的几个屏幕截图。为充分利用本教程,我建议您多看几遍,直到您对自己创建这个应用程序感到满意为止。本教程假定您有 HTML 和 JavaScript 经验。如果您是 JavaScript 新手,JavaScript Tutorial。您还可以在 GitHub 上的https://github.com/joshcrawmer4/Javascript-MVC-App上找到该项目。来吧,开始吧!
开始编写一些代码
创建一个index.html文件,其中包含以下代码:
<!doctype html>
<html>
<head>
<title>Javascript MVC</title>
<script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
<style>
</style>
</head>
<body>
<div class="js-container">
<input type="text" class="js-task-textbox">
<input type="button" class="js-add-task-button" value="Add Task">
<div class="js-tasks-container"></div>
<!-- end tasks -->
<input type="button" class="js-complete-task-button" value="Complete Tasks">
<input type="button" class="js-delete-task-button" value="Delete Tasks">
</div>
<!-- end js-container -->
<script src="EventDispatcher.js"></script>
<script src="TaskModel.js"></script>
<script src="TaskView.js"></script>
<script src="TaskController.js"></script>
<script src="App.js"></script>
</body>
</html>
接下来使用以下代码创建一个EventDispatcher.js文件:
var Event = function (sender) {
this._sender = sender;
this._listeners = [];
}
Event.prototype = {
attach: function (listener) {
this._listeners.push(listener);
},
notify: function (args) {
for (var i = 0; i < this._listeners.length; i += 1) {
this._listeners[i](this._sender, args);
}
}
};
接下来使用以下代码创建一个TaskModel.js文件:
var TaskModel = function () {
this.tasks = [];
this.selectedTasks = [];
this.addTaskEvent = new Event(this);
this.removeTaskEvent = new Event(this);
this.setTasksAsCompletedEvent = new Event(this);
this.deleteTasksEvent = new Event(this);
};
TaskModel.prototype = {
addTask: function (task) {
this.tasks.push({
taskName: task,
taskStatus: 'uncompleted'
});
this.addTaskEvent.notify();
},
getTasks: function () {
return this.tasks;
},
setSelectedTask: function (taskIndex) {
this.selectedTasks.push(taskIndex);
},
unselectTask: function (taskIndex) {
this.selectedTasks.splice(taskIndex, 1);
},
setTasksAsCompleted: function () {
var selectedTasks = this.selectedTasks;
for (var index in selectedTasks) {
this.tasks[selectedTasks[index]].taskStatus = 'completed';
}
this.setTasksAsCompletedEvent.notify();
this.selectedTasks = [];
},
deleteTasks: function () {
var selectedTasks = this.selectedTasks.sort();
for (var i = selectedTasks.length - 1; i >= 0; i--) {
this.tasks.splice(this.selectedTasks[i], 1);
}
// clear the selected tasks
this.selectedTasks = [];
this.deleteTasksEvent.notify();
}
};
接下来使用以下代码创建一个TaskView.js文件:
var TaskView = function (model) {
this.model = model;
this.addTaskEvent = new Event(this);
this.selectTaskEvent = new Event(this);
this.unselectTaskEvent = new Event(this);
this.completeTaskEvent = new Event(this);
this.deleteTaskEvent = new Event(this);
this.init();
};
TaskView.prototype = {
init: function () {
this.createChildren()
.setupHandlers()
.enable();
},
createChildren: function () {
// cache the document object
this.$container = $('.js-container');
this.$addTaskButton = this.$container.find('.js-add-task-button');
this.$taskTextBox = this.$container.find('.js-task-textbox');
this.$tasksContainer = this.$container.find('.js-tasks-container');
return this;
},
setupHandlers: function () {
this.addTaskButtonHandler = this.addTaskButton.bind(this);
this.selectOrUnselectTaskHandler = this.selectOrUnselectTask.bind(this);
this.completeTaskButtonHandler = this.completeTaskButton.bind(this);
this.deleteTaskButtonHandler = this.deleteTaskButton.bind(this);
/**
Handlers from Event Dispatcher
*/
this.addTaskHandler = this.addTask.bind(this);
this.clearTaskTextBoxHandler = this.clearTaskTextBox.bind(this);
this.setTasksAsCompletedHandler = this.setTasksAsCompleted.bind(this);
this.deleteTasksHandler = this.deleteTasks.bind(this);
return this;
},
enable: function () {
this.$addTaskButton.click(this.addTaskButtonHandler);
this.$container.on('click', '.js-task', this.selectOrUnselectTaskHandler);
this.$container.on('click', '.js-complete-task-button', this.completeTaskButtonHandler);
this.$container.on('click', '.js-delete-task-button', this.deleteTaskButtonHandler);
/**
* Event Dispatcher
*/
this.model.addTaskEvent.attach(this.addTaskHandler);
this.model.addTaskEvent.attach(this.clearTaskTextBoxHandler);
this.model.setTasksAsCompletedEvent.attach(this.setTasksAsCompletedHandler);
this.model.deleteTasksEvent.attach(this.deleteTasksHandler);
return this;
},
addTaskButton: function () {
this.addTaskEvent.notify({
task: this.$taskTextBox.val()
});
},
completeTaskButton: function () {
this.completeTaskEvent.notify();
},
deleteTaskButton: function () {
this.deleteTaskEvent.notify();
},
selectOrUnselectTask: function () {
var taskIndex = $(event.target).attr("data-index");
if ($(event.target).attr('data-task-selected') == 'false') {
$(event.target).attr('data-task-selected', true);
this.selectTaskEvent.notify({
taskIndex: taskIndex
});
} else {
$(event.target).attr('data-task-selected', false);
this.unselectTaskEvent.notify({
taskIndex: taskIndex
});
}
},
show: function () {
this.buildList();
},
buildList: function () {
var tasks = this.model.getTasks();
var html = "";
var $tasksContainer = this.$tasksContainer;
$tasksContainer.html('');
var index = 0;
for (var task in tasks) {
if (tasks[task].taskStatus == 'completed') {
html += "<div style="color:green;">";
} else {
html += "<div>";
}
$tasksContainer.append(html + "<label><input type="checkbox" class="js-task" data-index="" + index + "" data-task-selected="false">" + tasks[task].taskName + "</label></div>");
index++;
}
},
/* -------------------- Handlers From Event Dispatcher ----------------- */
clearTaskTextBox: function () {
this.$taskTextBox.val('');
},
addTask: function () {
this.show();
},
setTasksAsCompleted: function () {
this.show();
},
deleteTasks: function () {
this.show();
}
/* -------------------- End Handlers From Event Dispatcher ----------------- */
};
接下来使用以下代码创建一个TaskController.js文件:
var TaskController = function (model, view) {
this.model = model;
this.view = view;
this.init();
};
TaskController.prototype = {
init: function () {
this.createChildren()
.setupHandlers()
.enable();
},
createChildren: function () {
// no need to create children inside the controller
// this is a job for the view
// you could all as well leave this function out
return this;
},
setupHandlers: function () {
this.addTaskHandler = this.addTask.bind(this);
this.selectTaskHandler = this.selectTask.bind(this);
this.unselectTaskHandler = this.unselectTask.bind(this);
this.completeTaskHandler = this.completeTask.bind(this);
this.deleteTaskHandler = this.deleteTask.bind(this);
return this;
},
enable: function () {
this.view.addTaskEvent.attach(this.addTaskHandler);
this.view.completeTaskEvent.attach(this.completeTaskHandler);
this.view.deleteTaskEvent.attach(this.deleteTaskHandler);
this.view.selectTaskEvent.attach(this.selectTaskHandler);
this.view.unselectTaskEvent.attach(this.unselectTaskHandler);
return this;
},
addTask: function (sender, args) {
this.model.addTask(args.task);
},
selectTask: function (sender, args) {
this.model.setSelectedTask(args.taskIndex);
},
unselectTask: function (sender, args) {
this.model.unselectTask(args.taskIndex);
},
completeTask: function () {
this.model.setTasksAsCompleted();
},
deleteTask: function () {
this.model.deleteTasks();
}
};
接下来使用以下代码创建一个App.js文件:
$(function () {
var model = new TaskModel(),
view = new TaskView(model),
controller = new TaskController(model, view);
});
概述
好的,那么每个文件中发生了什么!
Index.html - 这里没有多少新东西。包括 jQuery CDN,设置一些基本的 HTML,以及加载我们将在这个项目中使用的 JavaScript 文件。哇!那很简单!
EventDispatcher.js - 这是一个有两个方法的类,attach()和notify()。attach()方法接受一个函数作为参数。您可以根据需要多次调用attach() ,并且您传递的函数可以包含您想要的任何代码。一旦您对该 Event 对象调用 notify 方法,您附加到该 Event 的每个函数都将运行。
TaskModel.js - 这个类有一些基本的方法来从任务数组中添加和删除实际的任务对象。在构造函数中设置三个 Event 对象,允许模型在添加、标记为完成或删除任务后对每个事件对象调用notify()方法。反过来,这将责任传递给视图以重新呈现 HTML 以显示更新的任务列表。要认识到的主要事情是模型将责任转移给了视图。模型 -> 视图。
TaskView.js - 这是这个项目中最大的文件,可以被抽象成多个视图。但为了简单起见,我把所有东西都放在一个类中。构造函数设置了五个 Event 对象。这允许视图调用每个 Event 对象的notify()方法,从而将责任传递给控制器。接下来,您会看到构造函数调用了init()方法。这个 init 方法使用方法链来建立这个类的主干。
createChildren() - 将$('.js-container') DOM 元素缓存在this.$container变量中,然后为之后需要find()的每个元素引用该变量。这只是一个性能问题,它允许 jQuery 从变量中提取任何元素,而不是重新查询/爬取 DOM。注意return this的使用。这允许在前一个init()调用中链接方法。
setupHandlers() - 这部分第一次绕着你的脑袋可能有点棘手。此方法设置事件处理程序并更改该处理程序内this关键字的范围。基本上,每当您遇到 JavaScript 事件处理程序并计划在该回调函数中使用非常著名的this关键字时,这将引用事件发生的实际对象或元素。这在许多情况下是不可取的,例如在 MVC 情况下,当您希望this引用实际类本身时。在这里,您在 JavaScript 回调函数上调用bind(this)方法。这改变了这个关键字范围指向类的范围,而不是初始化该事件的对象或元素。Mozilla Foundation 有一个很好的教程来解释如何使用范围绑定函数。
enable() - 此方法设置任何 DOM 事件并将任何函数附加到由模型创建的事件调度程序。看这行代码:
this.model.addTaskEvent.attach(this.addTaskHandler);
这里实际发生了什么?当模型调用addTaskEvent.notify()时,您的视图将运行this.addTaskHandler()方法。甜的!您实际上看到了 EventDispatcher 的工作原理!这允许您的类在保持解耦的同时相互交谈。然后addTaskHandler()方法调用show()方法,该方法又调用buildList()方法并重新呈现 HTML 列表。
那么你应该从这一切中得到什么?基本上,一旦模型将职责传递给视图,视图就会重新渲染 HTML 以显示最新的任务对象。此外,每当用户通过 DOM 事件操作视图时,视图就会将责任转交给控制器。视图不能直接与模型一起使用。
TaskController.js - 这个类位于视图和模型之间,充当将它们绑定在一起的粘合剂。它允许您轻松解耦模型和视图。每当视图使用 EventDispatcher 时,控制器都会在那里监听并更新模型。除此之外,这个文件中的所有方法声明应该看起来与视图和模型比较相似。
App.js - 此文件负责启动应用程序。Model-View-Controller 的一个实例在这里被创建。
结论
作为开发人员,您必须始终努力保持最新状态。无论是参与 GitHub 上的项目、涉足一种新语言,还是学习设计模式和原则。但有一件事是肯定的,你必须一直在前进。现在您已经对如何以 MVC 方式编写 JavaScript 有了基本的了解,您将了解如何停止编写意大利面条式代码以及如何开始编写更清晰、更易于维护的代码。请随时在 GitHub 上与我联系,或在下面的评论部分中发布任何问题。