前端mvc mvp mvvm 架构介绍(vue重构项目一)

首先 我们为什么重构这个项目

1:我们现有的技术是前后台不分离,页面上采用esayUI+jq构成的单页面,每个所谓的单页面都是从后台胜场的唯一Id 与前端绑定,即使你找到了那个页面元素,也找不到所在的文件,因为这个id是随机生成的,而页面的id绑定是由后台传回来的Id获得。

2:前后端项目融合在一起,UI框架与jQ使用,对于前端开发人员来说,从最简单的项目启动与调试,都是很繁琐的事情。

3:jsp与后台的架构混杂,文件分离过于复杂,耦合性还很高,针对某个显示出来的页面,我们很难定位到其所在,即使连个路径,都需要从后台获取。

4:开发效率低下,老的技术虽然考验程序员的基本功,但是代码繁杂却又啰嗦,可以看到,在开发时想快速开发 所有的代码都是粘贴复制的。为现在的开发 留下了很多的技术债。

怎么重构这个新的项目呢

由上面的介绍可以了解到,框架的出现是必然的,可以为开发人员提供一个可以快速构建准确且美观简洁的web应用的技术势必受到所有人的追捧,慢慢的前端就形成了现在三大框架 angular vue react 鼎立的局面。

首先 我们不分析这三个框架,我们来看下前端的一些架构上面的基础知识。

前端MVC设计模式

首先 最初这个设计模式是来自后端的java架构,用一张图来表示

前端mvc mvp mvvm 架构介绍(vue重构项目一)

这样设计的好处就是,每一层专注于做一件事情,层与层之间保持松耦合,每层做测试也方便,代码的维护性也会变得很好,前端的代码模块化会很明显。

1> mvc:

先来看看《基于MVC的JavaScript Web富应用开发》对MVC的定义——

MVC是一种设计模式,它将应用划分为3个部分:数据(模型 ajax)、展现层(视图 html)和用户交互(控制器 js/事件 有种说法叫做触发器)。

换句话说,一个事件的发生是这样的过程:

1. 用户和应用产生交互。

2. 控制器的事件处理器被触发。

3. 控制器从模型中请求数据,并将其交给视图。

4. 视图将数据呈现给用户。 我们不用类库或框架就可以实现这种MVC架构模式。关键是要将MVC的每部分按照职责进行划分,将代码清晰地分割为若*分,并保持良好的解耦。这样可以对每个部分进行独立开发、测试和维护。

关于mvc的框架,如 Embejs angularjs backbonejs Knockoutjs 等等--

前端mvc mvp mvvm 架构介绍(vue重构项目一)

通过上图,我们我们可以清楚地了解Javascript MVC框架之间的特性,复杂度和学习曲线的区别,从左到右我们了解到各个Javascript MVC框架是否支持数据绑定(Data Binding)、模板(Templating)和持久化等特性,从下到上MVC框架的复杂性递增。
  当然,“我们不用类库或框架就可以实现这种MVC架构模式。”因此,我们需要对MVC的每一个部分,做一个详细的剖析——
  1> 模型——

模型用来存放应用的所有数据对象。比如,可能有一个User模型,用以存放用户列表、他们的属性及所有与模型有关的逻辑。
模型不必知道视图和控制器的逻辑。任何事件处理代码、视图模板,以及那些和模型无关的逻辑都应当隔离在模型之外。
将模型的代码和视图的代码混在一起,是违反MVC架构原则的。模型是最应该从你的应用中解耦出来的部分。
当控制器从服务器抓取数据或创建新的记录时,它就将数据包装成模型实例。也就是说,我们的数据是面向对象的,任何定义在这个数据模型上的函数或逻辑都可以直接被调用。

  2> 视图——

视图层是呈现给用户的,用户与之产生交互。在JavaScript应用中,视图大都是由HTML、CSS、JavaScript模板组成的。除了模板中简单的条件语句之外,视图不应当包含任何其他逻辑。 将逻辑混入视图之中是编程的大忌,这并不是说MVC不允许包含视觉呈现相关的逻辑,只要这部分逻辑没有定义在视图之内即可。我们将视觉呈现逻辑归类为“视图助手”(helper):和视图相关的独立的小工具函数。

来看下面的例子,其在视图中包含了逻辑,这是一个范例,平时不应当这样做:

        <div>
<script>
function formatDate(data) {
/*....*/
}
</script>
${ formatDate(new Date()) }
</div>

但是在我们的现系统之内,常见的就是 这种将逻辑混淆在视图之内,整个页面看起来就是一个标签的大杂烩,混乱不堪, 难以查找一些逻辑代码,这也是违反了mvc初衷。

再来看下面一段代码,将视觉呈现逻辑剥离出来放入视图助手中,可以让这个应用结构满足mvc。

        /* 外部文件 helper.js */

            var helper = {};
helper.formateDate = function (date) {
/* */
}
/* template.html 页面 */
<div>
${ helper.formateDate(new Date()) }
</div>

此外,该写法实现了一个功能函数对象,将所有视觉呈现逻辑都包含在helper变量中,这是一个命名空间,可以防止冲突并保持代码清晰、可扩展。

3> 控制器——

控制器是模型和视图之间的纽带。控制器从视图获取事件和输入,对它们(很可能包含模型)进行处理,并相应地更新视图。当页面加载时,控制器会给视图添加事件监听,比如监听表单提交或按钮点击。然后,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。

我们用简单的jQuery代码来实现控制器——

        var controll={};
(controller.users = function ($) { var nameClick = function () {
/*...*/
} $(function() {
$('#view .name').click(nameClick)
});
})(jQuery)
现在,我们知道了M(Model)、V(View)、C(Controller)每个部分的工作内容,我们就可以轻松实现属于我们自己的MVC应用程序了,当然,我们完全不必依赖那些流行与否的MVC框架。
 

下面再看一张图:

前端mvc mvp mvvm 架构介绍(vue重构项目一)

MVC的View直接与Model打交道,Controller仅仅起一个“桥梁”作用,它负责把View的请求转发给Model,再负责把Model处理结束的消息通知View。Controller就是一个消息分发器。不传递数据(业务结果),Controller是用来解耦View和Model的,具体一点说,就是为了让UI与逻辑分离(界面与代码分离)。但是存在的不好的点是 控制层会变的太臃肿,所有视图的变化都在controller层去进行。

一对C-V捆绑起来表示一个ui组件,C直接接受用户输入,并将输入转为相应命令来调用M的接口,对M的状态进行修改,最后通过观察者模式对V进行重新渲染。

2> MVP

先来看一张图 表示下mvp的关系

前端mvc mvp mvvm 架构介绍(vue重构项目一)

  1. Mode--  数据层   业务逻辑和实体类 存储业务的数据
  2. View--  视图层   页面的展示
  3. p-- Presenter 逻辑层   包括数据和视图层交互  (叫派发器)

虽然在MVC里,View是可以直接访问Model的,但MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。

与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。

先看一个需求

前端mvc mvp mvvm 架构介绍(vue重构项目一)

Model层

myapp.Model = function() {
var val = ; this.add = function(v) {
if (val < ) val += v;
}; this.sub = function(v) {
if (val > ) val -= v;
}; this.getVal = function() {
return val;
};
};

Model层依然是主要与业务相关的数据和对应处理数据的方法。

view层

myapp.View = function() {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease'); this.render = function(model) {
$num.text(model.getVal() + 'rmb');
}; this.init = function() {
var presenter = new myapp.Presenter(this); $incBtn.click(presenter.increase);
$decBtn.click(presenter.decrease);
};
};
MVP定义了Presenter和View之间的接口,用户对View的操作都转移到了Presenter。比如这里可以让View暴露setter接口以便Presenter调用,待Presenter通知Model更新后,Presenter调用View提供的接口更新视图。

Presenter

myapp.Presenter = function(view) {
var _model = new myapp.Model();
var _view = view; _view.render(_model); this.increase = function() {
_model.add();
_view.render(_model);
}; this.decrease = function() {
_model.sub();
_view.render(_model);
};
};
Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很,维护起来会比较困难。而且由于没有数据绑定,如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。
运行程序时,以view为入口
(function() {
var view = new myapp.View();
view.init();
})();

3> mvvm

ViewModel指 "Model of View"——视图的模型。这个概念曾在一段时间内被前端圈热炒,以至于很多初学者拿jQuery和Vue做对比...

前端mvc mvp mvvm 架构介绍(vue重构项目一)

MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。

这里我们使用Vue来完成这个栗子。

Model

在MVVM中,我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为(格式化数据由View的负责),这里可以把它理解为一个类似json的数据对象。

var data = {
val:
};
 

View

和MVC/MVP不同的是,MVVM中的View通过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。写法如下:

<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>
 

ViewModel

ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心就是数据绑定,后面将会讲到。与MVP不同的是,没有了View为Presente提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。

new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < ) {
this.val += v;
}
},
sub(v) {
if(this.val > ) {
this.val -= v;
}
}
}
}); 

整体来看,比MVC/MVP精简了很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新(以前用jQuery操作DOM很繁琐)的问题。因为在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式可以使开发过程更加容易,提高应用的可重用性。

数据绑定

 
双向数据绑定,可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。——《界面之下:还原真实的MV*模式》

前端mvc mvp mvvm 架构介绍(vue重构项目一)

在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。

不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的前端框架实现数据绑定的方式大致有以下几种:

  • 数据劫持 (Vue)
  • 发布-订阅模式 (Knockout、Backbone)
  • 脏值检查 (Angular)

我们这里主要讲讲Vue。

Vue采用数据劫持&发布-订阅模式的方式,通过ES5提供的 Object.defineProperty() 方法来劫持(监控)各属性的 gettersetter ,并在数据(对象)发生变动时通知订阅者,触发相应的监听回调。并且,由于是在不同的数据上触发同步,可以精确的将变更发送给绑定的视图,而不是对所有的数据都执行一次检测。要实现Vue中的双向数据绑定,大致可以划分三个模块:Observer、Compile、Watcher,如图:

前端mvc mvp mvvm 架构介绍(vue重构项目一)

  • Observer 数据监听器
    负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。

  • Compiler 指令解析器
    扫描模板,并对指令进行解析,然后绑定指定事件。

  • Watcher 订阅者
    关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。Update()是它自身的一个方法,用于执行Compile中绑定的回调,更新视图。

数据劫持

一般对数据的劫持都是通过Object.defineProperty方法进行的,Vue中对应的函数为 defineReactive ,其普通对象的劫持的精简版代码如下:

 
var foo = {
name: 'vue',
version: '2.0'
} function observe(data) {
if (!data || typeof data !== 'object') {
return
}
// 使用递归劫持对象属性
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
})
} function defineReactive(obj, key, value) {
// 监听子属性 比如这里data对象里的 'name' 或者 'version'
observe(value) Object.defineProperty(obj, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newVal) {
if (value === newVal) {
return
} else {
value = newVal
console.log(`监听成功:${value} --> ${newVal}`)
}
}
})
} observe(foo)
foo.name = 'angular' // “监听成功:vue --> angular”

上面完成了对数据对象的监听,接下来还需要在监听到变化后去通知订阅者,这需要实现一个消息订阅器 Dep ,Watcher通过 Dep 添加订阅者,当数据改变便触发 Dep.notify() ,Watcher调用自己的 update() 方法完成视图更新。

写着写着发现离主题越来越远了。。。数据劫持就先讲这么多吧~对于想深入vue.js的同学可以参考勾三股四的Vue.js 源码学习笔记

总结

 
 MV*的目的是把应用程序的数据、业务逻辑和界面这三块解耦,分离关注点,不仅利于团队协作和测试,更有利于甩锅维护和管理。 业务逻辑不再关心底层数据的读写,而这些数据又以对象的形式呈现给业务逻辑层。 从 MVC --> MVP --> MVVM,就像一个打怪升级的过程,它们都是在MVC的基础上随着时代和应用环境的发展衍变而来的。 在我们纠结于使用什么架构模式或框架的时候,不如先了解它们。静下来思考业务场景和开发需求,不同需求下会有最适合的 解决方案。我们使用这个框架就代表认同它的思想,相信它能够提升开发效率解决当前的问题, 而不仅仅是因为大家都在学。
上一篇:SpringBoot揭秘:快速构建微服务体系


下一篇:C# WebApi 获取客户端ip地址