我对 MVC 的一己之见

644 阅读4分钟

MVC 是什么?

MVC 是一种架构设计模式,它通过关注点分离,鼓励改进应用程序组织。基于 DRY 原则,为实用、通用的代码取一个名,这就是设计模式。

在实践中,存在着代码级重复与页面级重复,分别对应着相似的代码与相似的页面两种情景。而 MVC 就是为了解决页面级重复而诞生的。

在 MVC 中,每个功能模块可以被分为三个对象,分别为 M、V、C,它们分别代表:

  • M(Model),负责操作数据。
  • V(View),负责操作 UI 界面。
  • C(Controller),负责网页中的其他部分。

MVC 并没有一个官方的、权威的定义,M、V、C 负责的内容、相互之间的关系也各有各的说法。

由于 V 和 C 有很深刻的联系,而 C 自带的属性太少了,所以前端往往会把 V 和 C整合在一起。

Model

即数据模型,用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,如:

const m = {  
    data: {  
        n: parseFloat(localStorage.getItem('module1.n')) || 100  
    },  
    create(){},  
    delete(){},  
    update(newData){  
        m.data.n = newData  
        localStorage.setItem('module1.n', `${m.data.n}`)  
        v.render()  
    },  
    get(){}  
}

Model 一般包含一个用于存储数据的对象 data,以及对数据进行增删改查的四个方法create()、delete()、update()、get()。

这些方法都没有固定的形式,依照需求的内容而定,但它们的大体步骤是一样的:

  1. 在 JS 引擎中处理数据。
  2. 将结果传递给渲染引擎进行渲染。

View

即视图,用于实现屏幕上的表示,描绘的是 Model 的当前状态,如:

const v = {  
    el: null,  
    html:`  
		<div class="output">  
		    <span id="number">{{n}}</span></div>  
		<div class="actions">  
		    <button id="add">+</button>
		    <button id="minus">-</button>
		</div>`,  
    init(container) {  
        v.el = $(container)  
    },  
    render(){  
        if(v.el.children.length !== 0) v.el.empty()  
        $(v.html.replace('{{n}}', m.data.n)).appendTo(v.el)  
    }  
}

View 一般包含一个 el、html 和 render() 方法。其中 el 为模块的监听对象,而 render() 为将 Model 中 data 变动后的数据渲染到网页当中的方法。

Controller

即控制器,用于定义用户界面对用户输入的响应方式,起到不同层面间的组织作用,用于控制应用程序的流程,它处理用户的行为和数据 Model 上的改变。

const c = {  
    init(container) {  
        v.init(container)  
        v.render(m.data.n)  
        c.autoBindEvents()  
    },  
    events: {  
        'click #add': 'add',  
        'click #minus': 'minus'  
    },  
    add(){  
        m.update(m.data.n + 1)  
    },  
    minus(){  
        m.update(m.data.n - 1)  
    },  
    autoBindEvents(){  
        for(let key in c.events){  
            const value = c[c.events[key]]  
            const spaceIndex = key.indexOf(' ')  
            const part1 = key.slice(0, spaceIndex)  
            const part2 = key.slice(spaceIndex + 1)  
            v.el.on(part1, part2, value)  
        }  
    }  
}

Controller 一般包含模块初始化方法 init()、“事件 对象: 方法”的 hash 表对象 events、对应的函数方法以及函数绑定事件 autoBindEvents()。

其中 init() 要实现三个功能:获得监听对象、在监听对象内部渲染、绑定事件。

MVC 中的抽象思维

最小知识原则

我们在 index.html 的主页中,引入的模块文件越少越好。从依赖 .html + .css + .js 引入一个模块,到最终只用一个 .js 文件来引入模块。

从模块外部观察,我们了解的模块内部信息越少越好。

我们在 index.html 中唯一引入一个 main.js 的 .js文件:

<script src="main.js"></script>

我们在 main.js 中引入 reset.css、global.css,以及模块文件 module1.js、module2.js:

import './reset.css'
import './global.css'

import m1 from './module1.js'
import m2 from './module2.js'

我们在 module1.js 中引入模块所需要使用的库 jquery 以及模块的样式文件 module1.css:

import './module1.css'
import $ from 'jquery'

更进一步地,我们可以只在 .html 文件中仅留下模块的监听对象,而将监听对象的内部元素以 String 的形式存储在 module.js 中,这样,从模块外部观察,我们对 module1 所能了解的仅有import m1 from './module1.js'

最小知识原则的缺点在于,在网速较慢的情况下,由于要加载多个文件,会导致页面一开始空白。对于这种情况我们可以添加旋转菊花、添加骨架、加占位内容等形式,让空白页面看起来没那么单调。

以不变应万变

用 M + V + C 来解决所有模块,不用再思考类似需求的实现方式。而由于 M、V、C 各存在一些共性,由此我们可以把它们封装成 class 文件,如 Model.js :

import EventBus from "./EventBus.js";

class Model{
  constructor(options) {
    Object.assign(this, options);
  }
  update() {
    console && console.error && console.error("你还没有实现这个方法");
  }
}

export default Model;

对于 constructor(),我们传入一个对象,里面包含了 Model 实例中所有属性以及需要重写的方法:

import Model from "./base/Model.js";

const  m = new Model({
    data: {  
        n: parseFloat(localStorage.getItem('module1.n')) || 100  
    },
	update(newData){  
        m.data.n = newData  
        localStorage.setItem('module1.n', `${m.data.n}`)  
        v.render()  
    }
})

但是只有简单需求的情况下,MVC 显得有些多余。而有一些特殊情况则会不知道怎么变通,比如有些模块没有 html。

表驱动编程

当出现大量类似但不重复的代码,我们可以把不重复的部分做成 hash 表对象,再通过遍历来完成重复的操作,如:

events: {  
    'click #add': 'add',  
    'click #minus': 'minus'  
},
add(){},
minus(){},
autoBindEvents(){  
    for(let key in c.events){  
        const value = c[c.events[key]]  
        const spaceIndex = key.indexOf(' ')  
        const part1 = key.slice(0, spaceIndex)  
        const part2 = key.slice(spaceIndex + 1)  
        v.el.on(part1, part2, value)  
    }  
}

事不过三

同样的代码写三遍,就应该抽成一个函数。

同样的属性写三遍,就应该做成共用属性。

同样的原型写三遍,就应该使用继承。

但有时候,可能会由于层级过深,而无法看懂代码。对于这个问题,我们可以使用画图或者文档来解决。

EventBus

为了实现两个对象之间的通信,我们可以使用 EventBus 来监听事件。

EventBus 本质是一个 juqery 对象:

const eventBus = $(window)

通过 .trigger().trigger() 和 .on() 可以实现对象之间的通信:

eventBus.trigger('m:updated')

eventBus.on('m:updated',()=>{
	view.render(m.data.n)
})

这主要用于监听多个事件对一个对象更改,类似的方法还有事件委托。

而我们往往也会把 EventBus 直接封装成一个 class,并通过继承,让 Model 和 View 直接成为一个 EventBus:

import $ from 'jquery'

class EventBus {
    constructor() {
        this._eventBus = $(window)
    }
    on(eventName, fn){
        return this._eventBus.on(eventName, fn)
    }

    trigger(eventName, data){
        return this._eventBus.trigger(eventName, data)
    }

    off(eventName, fn){
        return this._eventBus.off(eventName, fn)
    }
}

export default EventBus

在 Model.js 和 View.js 中我们继承这个 EventBus 对象:

import EventBus from "./EventBus";

class Model extends EventBus{
    constructor(options) {
        super()
    }
}

由此,在模块中我们可以直接通过 Model 和 View 对象来调用 .trigger() 和 .on(),而无需事先创建一个 EventBus 对象。

使用 EventBus 类的好处在于事先了解耦、把模块之间关联性降到最低,方便日后重构代码。

我们为什么需要模块化?

模块化,简而言之就是把部分代码抽取出来封装到一个 .js 文件中,之后通过 export 导出、import 导入这部分代码。

模块化使得各个功能之间解耦,每个模块都不需要知道另一个模块的具体功能,甚至每个模块都可以使用不同的方法实现。

这在多人协同的大型项目中具有极大的作用,每个功能的负责程序员无需知道另一个功能是如何实现的、担心自己的代码是否会干扰到另一个模块中的代码。

另一方面,这也方便日后对代码的重构。通过模块化把各个功能区隔离开来,我们完全可以只对整体功能中的一部分进行重构,而无需对整个程序进行重构,这极大提高了代码的可维护性。

并且,通过模块化,我们可以把部分通用的代码抽取出来,放入一个原型或者类中,从而提高了代码的复用性。