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

时间:2022-09-20 21:15:49

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

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 = 0;

    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) 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(1);
        _view.render(_model);
    };

    this.decrease = function() {
        _model.sub(1);
        _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: 0
};

 

 

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 < 100) {
                this.val += v;
            }
        },
        sub(v) {
            if(this.val > 0) {
                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的基础上随着时代和应用环境的发展衍变而来的。 在我们纠结于使用什么架构模式或框架的时候,不如先了解它们。静下来思考业务场景和开发需求,不同需求下会有最适合的 解决方案。我们使用这个框架就代表认同它的思想,相信它能够提升开发效率解决当前的问题, 而不仅仅是因为大家都在学。