阅读 133

MVC初体验

写在前面

相信刚开始自学前端的同学一定经常听到 MVC 这个名词,是不是听上去觉得很高大上,很难懂的样子。。。
没错,MVC是很高大上,但是并没有你想象的那么难懂。
今天就带大家简单了解一下什么是MVC,以及MVC在实际开发中是怎么使用的。

什么是MVC

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码。

将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

具体来说,M、V、C的功能分别如下:

  • 视图(view)是用户看到并与之交互的界面。
  • 模型(model)表示数据模型,并提供数据给视图。
  • 控制器(controller)是连接视图和模型桥梁,处理业务逻辑操作,具体是指接受用户的输入并调用模型和视图去完成用户的需求。


1589766902522-1af6679e-dfde-4732-a09b-274197c0ff1c.png


三者之间的数据关系:

  • View 接受用户交互请求
  • View 将请求转交给Controller处理
  • Controller 操作Model进行数据更新保存
  • 数据更新保存之后,Model会通知View更新
  • View 更新变化数据使用户得到反馈

MVC模型(简化版)

我们先来看下MVC的模型是怎样的。

//页面加载后创建MVC对象
$(function(){
    //创建MVC对象
    var MVC=MVC||{};
    //初始化MVC数据模型层
    MVC.model=function(){}();
    //初始化MVC视图层
    MVC.view=function(){}();
    //初始化MVC控制器层
    MVC.controller=function(){}();
});
复制代码
  • Model
//MVC数据模型层
MVC.model=function(){
	//内部数据对象
    var M={};
    //服务器端获取数据,通常通过Ajax获取并存储
    M.data={};
    //配置数据
    M.config={};
    return {
    	//获取服务器端数据
    	getData:function(m){
    		return M.data[m];
    	},
    	//获取配置数据
    	getConfig:function(c){
    		//根据数据字段获取数据
    		return M.config[c]
    	},
    	//设置服务器数据
    	setData:function(m,v){
    		M.data[m]=v;
    		return this;
    	},
    	//设置配置数据
    	setConfig:function(c,v){
    		M.data[c]=v;
    		return this;
    	}
    };
}();
复制代码
  • View
//MVC视图层
MVC.view=function(){
	//模型数据层对象操作方法引用
	var M=MVC.model;
	//内部视图创建方法对象
	var V={};
	//获取视图的接口方法
	return function(v){
		//根据视图名词返回视图
		V[v]();
	}
}();
复制代码
  • Controller
//MVC控制器层
MVC.controller=function(){
	//模型数据层对象操作方法引用
	var M=MVC.model;
	//视图数据层对象操作方法引用
	var V=MVC.view;
	//控制器创建方法对象
	var C={};
}();
复制代码

实例讲解

看到上面这个死板的模型是不是觉得还是难懂,没关系,接下来就用一个实例来说明。

我们有这么一个需求:网页上有4个button,功能分别是加、减、乘、除,另外还有一个输出框表示输出,默认值是100。今天我们通过MVC将传统的设计方式变成模块化的设计方式。

案例引入

首先,在文件src下面建立index.html、main.js、style.css;

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
    />
    <title>MVC demo</title>
    <link rel="stylesheet" href="style.css" />
  </head>
<body>
  <section id="app">
    <div class="output">
    	<span class="numbers">n</span>
    </div>
    <div class="actions">
      <button class="add">+1</button>
      <button class="reduce">-1</button>
      <button class="mul">*2</button>
      <button class="device">/2</button>
    </div>
   </section>
  <script src="main.js"></script>
</body>
</html>
复制代码
  • style.css
#app1 {
  width: 50vw;
  height: 50vh;
}
#app1 .output {
}
#app1 .actions {
}
复制代码
  • main.js
const $button1 = $("#add1");
const $button2 = $("#minus1");
const $button3 = $("#mul2");
const $button4 = $("#divide2");
const $number = $("#number");
const n = localStorage.getItem("n");
$number.text(n || 100);

$button1.on("click", () => {
  let n = parseInt($number.text());
  n += 1;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button2.on("click", () => {
  let n = parseInt($number.text());
  n -= 1;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button3.on("click", () => {
  let n = parseInt($number.text());
  n *= 2;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button4.on("click", () => {
  let n = parseInt($number.text());
  n /= 2;
  localStorage.setItem("n", n);
  $number.text(n);
});
复制代码


好了,大家看看上面的代码,有什么感觉?

是不是有大量的代码重复。

有的同学可能会说,这几行的重复似乎什么。

没错,目前我们要实现的这个功能比较简单,二三十行代码就可以搞定。但是如果我们的功能变得复杂,那代码之间的逻辑就变得尤为重要。


所以,我们要减少重复

什么叫重复:

  • 代码级别:你把相同的代码写了三遍,那么你就应该重构它
  • 页面级别:你把相同的页面做了10遍,那么你就应该想一个“万金油”的写法


MVC就是一个“万金油”,他可以帮助你优化代码结构。

那么我们接下来开始优化代码。

优化代码

第一步:
我们都知道要想展示一个动态页面,必须要有html、css、js。我们一般的做法是在html中写好网页结构、再引入css和js。

这样做肯定是没问题的,但是如果网页变多的话,那么html页面一定不堪重负。

那么有什么好的解决办法吗?有的。

我们可以用JavaScript中的模块化功能import和export

现在只需要在js代码开头引入 只和这个页面相关的文件 ,不相关的我们就不去管,需要知道的知识越少越好。

这叫 最小知识原则 。

因此,我们可以给这个功能一个单独的js的文件,在main.js中引入即可,html中只需引入一次main.js即可。这样如果新增别的功能也可以分别管理、互不干扰。

  • app1.js
import './style.css' //引入该页面的css
import $ from 'jquery'  //引入jquery

const $button1 = $("#add1");
const $button2 = $("#minus1");
const $button3 = $("#mul2");
const $button4 = $("#divide2");
const $number = $("#number");
const n = localStorage.getItem("n");
$number.text(n || 100);

$button1.on("click", () => {
  let n = parseInt($number.text());
  n += 1;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button2.on("click", () => {
  let n = parseInt($number.text());
  n -= 1;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button3.on("click", () => {
  let n = parseInt($number.text());
  n *= 2;
  localStorage.setItem("n", n);
  $number.text(n);
});
$button4.on("click", () => {
  let n = parseInt($number.text());
  n /= 2;
  localStorage.setItem("n", n);
  $number.text(n);
});
复制代码
  • main.js
import "./reset.css"; //引入重置css样式
import "./global.css";  //引入全局样式

import "./app1.js";  //引入app1的js

//如果新增其他功能可以依次引入,举例:
import "./app2.js";
import "./app3.js";
import "./app4.js";
复制代码

第二步:
完成了上述功能后,我们开始用MVC进行改写。

  • Model数据

数据相关的部分都放到这里。

现在Model中包括:

  1. 属性有data——存放在localstorage中的变量。
  2. 方法——增、删、改、查(这里写的是通用的模块,虽然本功能不涉及增、删和查)。
const m = {
  data: {
    n: parseInt(localStorage.getItem('n'))
  },//获取数据
  create() {},//增
  delete() {},//删
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')
    localStorage.setItem('n', m.data.n)
  },//改
  get() {}
}//查
}
复制代码
  • View视图

视图主要是渲染到页面

那么我们是否可以将html节点也写到这里呢?

是可以的,因为html里的内容更新,view也会改变。

那么现在view中包括:

  1. 属性el——用于接收更新的节点容器
  2. 属性html——用于存放待添加的节点
  3. 方法init——初始化容器
  4. 方法render——判断容器的后代是否存在,再重新渲染页面
const v = {
  el: null,
  html: `//生成HTML
  <div>
    <div class="output">
      <span id="number">{{n}}</span>
    </div>
    <div class="actions">
      <button id="add1">+1</button>
      <button id="minus1">-1</button>
      <button id="mul2">*2</button>
      <button id="divide2">÷2</button>
    </div>
  </div>
`,
  init(container) {
    v.el = $(container)
  },
  render(n) {
    if (v.el.children.length !== 0) v.el.empty()
    $(v.html.replace('{{n}}', n))
      .appendTo(v.el)
  }
}
复制代码
  • Controller控制器

C主要是连接View和Model的桥梁,处理业务逻辑操作。

现在Controller中包括:

  1. 属性events——多个点击事件用哈希表来展示
  2. 方法init——初始化容器后,再自动绑定事件,重新渲染
  3. 方法加、减、乘、除——触发点击事件后将m中的数据进行更新
  4. 方法autoBindEvents——为表中的数据添加点击事件
const c = {
//初始化容器
  init(container) {
    v.init(container)
    v.render(m.data.n) // view = render(data)
    c.autoBindEvents()//自动绑定事件
    eventBus.on('m:updated', () => {  //运用了eventBus,下面会补充说明
      console.log('here')
      v.render(m.data.n)
    })//监听数据的变化,重新渲染到页面
  },
//事件太多,通过哈希表来一一列出,也就是后面提到的表驱动编程
  events: {
    'click #add1': 'add',
    'click #minus1': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
  },
//每个事件点击对应着数据变化的操作函数
  add() {
    m.update({n: m.data.n + 1})
  },
  minus() {
    m.update({n: m.data.n - 1})
  },
  mul() {
    m.update({n: m.data.n * 2})
  },
  div() {
    m.update({n: m.data.n / 2})
  },
  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)
    }
  }
}
export default c
复制代码


看到这里,我们已经根据M、V、C各自的功能对代码完成了初步的优化。

相信同学们应该可以直观的看出来,C中的加、减、乘、除方法就是我们对初始代码简化的一个结果(避免重复)。

这么写有一个专业的叫法,称为 表驱动编程 。

这里的表就是 哈希表 (一种非常好用的数据结构)。

像这样,把重要的代码抽离出来,放到一个哈希表里,你的代码会变得清爽,且易于维护。

第三步:
好了,我们还可以继续优化吗?

还可以。那么还能怎样优化?

这里要先给大家引入一个“事不过三”的概念,即:

  • 同样的代码写三遍,就应该抽成一个函数
  • 同样的方法写三遍,就应该做成一个共用属性(原型或类)
  • 同样的原型写三遍,就应该用继承

比如,如果我们做了多个功能,我们在每个功能对应的js中都要写一遍M、V、C的方法,那么这个时候就可以把这些方法写成一个类。

举例说明一下:

  • Model.js
class Model {
  constructor(options) {
    ['data', 'update', 'create', 'delete', 'get'].forEach((key) => {  //这五个都可以自定义
      if (key in options) {
        this[key] = options[key]
      }
    }
  }

  create() {
    console && console.error && console.error('你还没有实现 create')
    //用可选链表示  console?.error ?.('你还没有实现 create')
  }

  delete() {
    console && console.error && console.error('你还没有实现 delete')
  }

  update() {
    console && console.error && console.error('你还没有实现 update')
  }

  get() {
    console && console.error && console.error('你还没有实现 get')
  }
}
复制代码
  • View.js
import $ from 'jquery'

class View {
  constructor({el, html, render}) {  //结构赋值
    this.el = $(el)
    this.html = html
    this.render = render
  }
}
复制代码

这里再给大家补充一个知识点,就是EventBus

EventBus(事件总线) 也是一种设计模式或框架,主要用于组件/对象间通信的优化简化。

EventBus里面涉及到很多API,下面我就列举几个常用,并对它们的用法进行分析。这个EventBus我们在运用的时候通常是这么来引入的:

const eventBus = $(window)
复制代码


在本例中我们也使用到了EventBus来实现M和C之间的通信。

由于EventBus几乎所有功能都要有的,所以我们可以将它写成一个原型,并让其他类来继承这个原型。

举例说明一下:

  • eventBus.js
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
复制代码
  • Modal继承Eventbus
import EventBus from './EventBus'
class Model extends EventBus {
  constructor(options) {
    super()
    const keys = ['data', 'update', 'create', 'delete', 'get']
    keys.forEach((key) => {
      if (key in options) {
        this[key] = options[key]
      }
    })
  }
  create() {
    console && console.error && console.error('你还没有实现 create')
  }
  delete() {
    console && console.error && console.error('你还没有实现 delete')
  }
  update() {
    console && console.error && console.error('你还没有实现 update')
  }
  get() {
    console && console.error && console.error('你还没有实现 get')
  }
}
export default Model
复制代码
  • View继承Eventbus
import $ from 'jquery'
import EventBus from './EventBus'
class View extends EventBus{
  // constructor({el, html, render, data, eventBus, events}) {
  constructor(options) {
    super() // EventBus#constructor()
    Object.assign(this, options)
    this.el = $(this.el)
    this.render(this.data)
    this.autoBindEvents()
    this.on('m:updated', () => {
      this.render(this.data)
    })
  }
  autoBindEvents() {
    for (let key in this.events) {
      const value = this[this.events[key]]
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex)
      const part2 = key.slice(spaceIndex + 1)
      this.el.on(part1, part2, value)
    }
  }
}
export default View
复制代码


EventBus中涉及到了三个API,分别是:on、trigger、off;

  • on:监听事件的变化

监听数据的变化,如果数据有变化,直接render(再次将变化后的数据渲染到页面)

this.on('m:updated', () => {
      this.render(this.data)
    })
复制代码
  • tirgger:自动触发事件
update(data) {
    Object.assign(m.data, data)//把传进来的data直接放在m.data上
    eventBus.trigger('m:updated')//通过trigger自动更新数据
    localStorage.setItem('n', m.data.n)//储存数据
}
复制代码
  • off:关闭的意思


好了,我们的代码优化到这里就已经差不多了,如果还想要继续优化的话,就要用到Vue相关的知识了。

模块化

这里再对模块化进行一个补充吧。

今天我们做的上面这些优化,都算是模块化的一种表现。

那什么是模块呢?

模块的定义:

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化的优点:

  • 多人协作互不干扰
    模块化避免了变量污染,并且可以使得分工更加容易
  • 灵活架构,焦点分离
    可以将独立的功能从主干中分离开来单独开发,增加效率
  • 方便模块间组合、分解 、解耦
    降低各个功能模块间的耦合度,方便维护和管理
  • 方便单个模块功能调试、升级

相关规范:

  1. commonJS
  • commonJS规范 NodeJS
  1. AMD
  • AMD规范 requireJS
  1. CMD
  • CMD规范 seaJs

总结

希望通过这篇文章能给大家对理解MVC和模块化有一定的帮助,如有描述不周的地方可以私聊我,欢迎交流~~~

参考