原文标题:Build A Simple Javascript App The MVC Way

作者:joshcrawmer4

翻译人:huansky

初次翻译,翻译的不好,还请见谅

JavaScript中最好的一部分之一,也可能是最糟糕的。 在HTML文档的头部添加一个开始和结束脚本标记,并在其中引入一些意大利面条式的代码,毫无疑问这是一种过分简单化的能力。 瞧! 有用! 真的吗? 你问什么是意大利面条式的代码? 意大利面条式的代码是一个直白的术语,指的是那些代码凌乱,控制结构复杂,并且这种代码到处都是。 它几乎不可能维护和调试,通常具有非常差的结构,并且容易出错。 网页设计和开发已经具有足够的挑战性,不要让你的工作变得更加困难!

那么你怎样才能结束编写这样的代码呢? 我认为,你只有两个选择。 再次声明,这只是我个人的意见。 第一个,你可以使用JavaScript框架。第二个,学习如何使用某种模式或结构编写JavaScript代码。 MVC,MVP和MVVM是一些常见模式,有助于指导开发人员创建抽象和解耦的解决方案。 这些模式之间的主要区别在于如何处理数据层,表示层和应用逻辑。 在这篇博文中,您将专注于MVC或模型 - 视图 - 控制器模式。

模型(数据层) - 这是为您的应用程序存储数据的位置。 模型与视图和控制器分离, 每当模型发生变化时,它将通知其观察者使用事件分派器发生了更改。 你会很快阅读关于事件调度程序。 在您将要构建的待办事项列表应用程序中,模型将保存任务列表,并负责对每个任务对象执行的任何操作。

视图(表示层) - 这一部分可以访问DOM,并负责事件处理程序,如:click,onmouseover,onmouseout等。视图也负责HTML的呈现。 在待办事项列表应用程序中,视图将负责向用户显示任务列表。 但是,每当用户通过输入新任务时,视图将使用事件分派器通知控制器,然后控制器将更新模型。 这能够让视图与模型快速解耦。

控制器(应用逻辑) - 控制器是模型和视图之间的粘合剂。 控制器处理和响应由模型或视图设置的事件。 它在用户操纵视图时更新模型,并且还可以在模型更改时更新视图。 在本教程中,您将通过分派事件直接从模型更新视图。 但是,不管哪一种方式都是可以的。 在待办事项列表应用程序中,当用户单击添加任务按钮时,点击将传递到控制器,控制器通过向其添加任务来修改模型。 任何其他决策或逻辑也可以在这里执行,例如:使用HTML本地存储保存数据,对数据库/服务器进行异步保存等等。

事件分派器 - 事件分派器是一个对象,该对象允许您将无限数量的函数/方法附加到它身上。 当你最终调用该事件对象的notify方法时,你所附加到该事件的每个方法都会被运行。 你将会在下面的代码中看到它被多次使用。

现在,您已经基本了解了为什么应该使用JavaScript设计模式,以及MVC架构是什么,现在您可以开始构建应用程序。 下面将简单介绍这个教程是怎么工作的。 首先,我将向您呈现代码。 这将给你机会检查和通过它。 第二,我将详细介绍代码使用的一些核心概念,并尝试阐明任何可能会产生混淆的潜在朦胧或灰色区域。 总而言之,我将向您展示最终组件的几个截图。 为了充分利用这个教程,我建议你好好看几遍,直到你能够自己编写这些代码。 本教程假设你有HTML和JavaScript经验。 如果你是JavaScript新手,我建议你快速看一下http://www.w3schools.com/Js/。 您还可以在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" ></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.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 (event) { 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; text-decoration:line-through'>";
} 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没理解,不绑定就会出现this.model未定义,是不是在赋值的时候出现this的指向的变化????
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()n 次。这个函数(这里指参数)的功能由你来决定。 一旦您调用了该Event对象的notify方法,附加到该事件的每个函数都将被运行。

TaskModel.js —— 这个类有一些基本方法来添加和删除实际任务的数组对象。 在构造函数中设置三个事件对象,当一个任务被添加后,标记为完成,或删除之后,允许模型调用每个事件对象的notify()方法。 转而将响应传递给视图来重新呈现HTML以显示更新的任务列表。 这里主要是模型将响应传递给视图。模型 --> 视图

TaskView.js -这是该项目最大的文件,并可能已被抽象成多个视图。 但是为了简单起见,我把一切都放到一个类中。 构造函数设置了五个Event对象。 这可以调用每个事件对象的notify()方法,从而传递响应到控制器。 接下来,你会看到构造函数调用 init()方法。 此 init 方法使用方法链建立此类的骨架。

createChildren()—— 在 this.$container 变量上缓存 $('.js-container') 这个DOM元素,后面需要访问具体的元素,它需要find()。 这只是一个性能事件,并允许jQuery从变量中拉取任何元素,而不是重新搜索/爬取DOM。 注意 reutrn this 的使用。 这是使得方法链存在 init 的主要原因。

setupHandlers() —— 这部分刚开始可能你会有点迷糊。这个方法用来设置 事件处理和改变内部 的 this 指向。 基本上,只要你碰到一个JavaScript事件处理程序,并打算在回调函数使用 this 这个关键字,那么这会引用事件发生的实际对象或元素。 这在很多情况下是不可取的,如MVC情况下,当你希望 this 引用实际类的本身。 在这里,你可以在回调函数上使用bind(this)。 这会 改变 this 作用域,而不是指向初始化该事件的对象或元素。 Mozilla基金会有一个很好的教程讲解如何使用 scope bound functions 。(这段话主要讲 this ,以及如何改变this的指向)

enable() —— 此方法设置的任何DOM事件,以及附加到 event Dispatcher 的任何方法都由模型来创建。 看下面这行代码:

this.model.addTaskEvent.attach(this.addTaskHandler);

这里到底发生了什么? 当模式调用 addTaskEvent.notify(), 视图将运行this.addTaskHandler()方法。 太棒了! 你实际上看到了 EventDispatcher 是怎样工作的! 这允许你的类彼此交谈,同时保持解耦。 然后,addTaskHandler()方法调用 show() 方法, show() 方法反过来调用 buildList()方法,最后重新呈现 HTML 列表。

那么,你应该从这些中得到什么? 基本上,一旦模型将数据传递给视图,视图将重新呈现HTML以显示最新的任务对象。 此外,每当用户通过DOM事件操纵视图时,视图会将响应传递给控制器​​。 视图不直接与模型一起工作。

TaskController.js —— 该类坐在视图和模型中间,充当粘合剂。 它能够让你的模型和视图轻松解耦。无论何时,当视图调用EventDispatcher时,控制器通过监听然后更新模型。 此外,这个文件中的所有方法声明和View和Model中看起来很像。

App.js —— 此文件是负责启动的应用程序。 在这里创建 Model-View-Controller 的一个实例。

结论

作为一名开发人员,您必须始终努力保持最新状态。 无论是参与GitHub上的项目,使用新语言,还是学习设计模式和原则。 但有一点是肯定的,你必须总是向前迈进。 现在你有一个基本的概述如何编写JavaScript的MVC方式,你看看如何可能结束编写意大利面条式的代码,以及如何开始编写更加整洁,更易于维护的代码。 随时在GitHub上与我联系,或有任何问题可发表在评论区。

翻译结束,个人想法:

个人分析

估计很多人,看到程序也是很难理解。我就添加下个人的解释好了

  • 在App.js里运行的时候,在创建view的实例的时候,会添加相应的事件处理函数,比如事件绑定,往model里面添加事件处理函数;
  • 接着开始创建controller的实例,值得注意的事,view中的model和controller中的model指向的是同一个。
  • 然后也开始添加相应的时间处理,比如,往view中添加相应的事件最后就是显示
  • 显示之后,用户点击视图的添加按钮,会调用相应的处理器,获取用户输入的数据。然后会告诉控制器,
  • 控制器呢会让model把这个数据添加进去,添加完成后,model会告诉view 我添加好了。这时候又会调用相应的处理器,最终显示
  • 其他类似

作者如果能在代码加一些注释就好了,不过作者的写法值得我学习,条理逻辑分的很清楚。

当然了,即使看懂了,你还要做的是,如果是你,你能写出来吗?也就是我们要分析作者是如何考虑的。

首先是MVC之间的通信是采用了观察者模式。

其次,我们就要分析显示的内容,以及相应的操作。

这是一个to-do-list,很明显就要有添加事件,删除事件,选中事件,完成事件等等。

一句话,就是你要把你要做的东西分析清楚,才好写代码。

【译】采用MVC模式创建一个简单的javascript App的更多相关文章

  1. 采用MVC模式创建一个简单的javascript App

    初次翻译,翻译的不好,还请见谅 JavaScript中最好的一部分之一,也可能是最糟糕的. 在HTML文档的头部添加一个开始和结束脚本标记,并在其中引入一些意大利面条式的代码,毫无疑问这是一种过分简单 ...

  2. 如何创建一个简单的C++同步锁框架(译)

    翻译自codeproject上面的一篇文章,题目是:如何创建一个简单的c++同步锁框架 目录 介绍 背景 临界区 & 互斥 & 信号 临界区 互斥 信号 更多信息 建立锁框架的目的 B ...

  3. 使用ssm(spring+springMVC+mybatis)创建一个简单的查询实例(二)(代码篇)

    这篇是上一篇的延续: 用ssm(spring+springMVC+mybatis)创建一个简单的查询实例(一) 源代码在github上可以下载,地址:https://github.com/guoxia ...

  4. 一个先进的App框架:使用Ionic创建一个简单的APP

    原文  http://www.w3cplus.com/mobile/building-simple-app-using-ionic-advanced-html5-mobile-app-framewor ...

  5. 程序猿修仙之路--数据结构之你是否真的懂数组? c#socket TCP同步网络通信 用lambda表达式树替代反射 ASP.NET MVC如何做一个简单的非法登录拦截

    程序猿修仙之路--数据结构之你是否真的懂数组?   数据结构 但凡IT江湖侠士,算法与数据结构为必修之课.早有前辈已经明确指出:程序=算法+数据结构  .要想在之后的江湖历练中通关,数据结构必不可少. ...

  6. 《Entity Framework 6 Recipes》翻译系列 (3) -----第二章 实体数据建模基础之创建一个简单的模型

    第二章 实体数据建模基础 很有可能,你才开始探索实体框架,你可能会问“我们怎么开始?”,如果你真是这样的话,那么本章就是一个很好的开始.如果不是,你已经建模,并在实体分裂和继承方面感觉良好,那么你可以 ...

  7. 无废话Android之listview入门,自定义的数据适配器、采用layoutInflater打气筒创建一个view对象、常用数据适配器ArrayAdapter、SimpleAdapter、使用ContentProvider(内容提供者)共享数据、短信的备份、插入一条记录到系统短信应用(3)

    1.listview入门,自定义的数据适配器 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/and ...

  8. 如何创建一个简单的struts2程序

    如何创建一个简单的Struts2程序 “计应134(实验班) 凌豪” 1.创建一个新的Web项目test(File->new->Web Project) 2.Struts2框架的核心配置文 ...

  9. WInform 创建一个简单的WPF应用

    (一)创建一个简单的WPF应用 首先,在这里我要说明的是:这里的例子,都是通过控制台程序来创建WPF应用,而非使用现成的WPF模版.因为WPF模版封装了创建WPF应用所需要的各种基本元素,并不利于我们 ...

随机推荐

  1. jboss developers studio 快速创建 spring mvc 项目

    1. 2. 部署运行 还有一个 rest very good !! ps:其实就是 一个 jboss 的 spring mvc maven 原型

  2. int 和 string 相互转换(简洁版)

    string int2str(int x) { return x ? num2str(x/10)+string(1,x%10+'0') : "";} int str2int(str ...

  3. Java学习笔记二——标识符和关键字

    标识符 定义 标识符的定义:对各种变量.方法和类等要素命名时使用的字符序列成为标识符. 简单地说,就是凡是自己可以起名字的地方都叫标识符,都要遵守标识符的规则. 命名规则 标识符只能由字母.下划线&q ...

  4. java-类加载器

    类加载器 用来加载Java类到Java虚拟机中.一般来说,Java虚拟机使用Java类的方式如下:Java 源程序(.java 文件)在经过Java编译器编译之后就被转换成字节码(.class 文件) ...

  5. linux添加新LUN,无需重启

    linux添加新LUN,无需重启 在给存储增加新的Lun时,在linux下一般是: A.重启操作系统B.重启HBA卡驱动 1. kudzu添加完新硬盘后,运行命令kudzu重新扫描新的硬件设备,类似a ...

  6. beetle.express针对websocket的高性能处理

    客户需要对websocket服务应用,其要求每秒同时给3W个在线的websocket连接进行广播消息.以下针对beetle.express扩展websocket简单的性能测试.从测试结果来看基本没什么 ...

  7. 【WPF】如何把一个枚举属性绑定到多个RadioButton

    一.说明 很多时候,我们要把一个枚举的属性的绑定到一组RadioButton上.大家都知道是使用IValueConverter来做,但到底怎么做才好? 而且多个RadioButton的Checked和 ...

  8. Matrix Admin 后台模板笔记

    一个后台模板用久了就想换一个.上次找到了Matrix Admin.和ACE一样都是Bootstrap风格,比较容易上手.Matrix要更健壮些.感觉拿去做用户界面也是可以的. 整体风格: 1.表单验证 ...

  9. (翻译)反射处理java泛型

    当我们声明了一个泛型的接口或类,或需要一个子类继承至这个泛型类,而我们又希望利用反射获取这些泛型参数信息.这就是本文将要介绍的ReflectionUtil就是为了解决这类问题的辅助工具类,为java. ...

  10. WCF基础教程之异常处理:你的Try..Catch语句真的能捕获到异常吗?

    在上一篇WCF基础教程之开篇:创建.测试和调用WCF博客中,我们简单的介绍了如何创建一个WCF服务并调用这个服务.其实,上一篇博客主要是为了今天这篇博客做铺垫,考虑到网上大多数WCF教程都是从基础讲起 ...