写在前面
相信刚开始自学前端的同学一定经常听到 MVC 这个名词,是不是听上去觉得很高大上,很难懂的样子。。。
没错,MVC是很高大上,但是并没有你想象的那么难懂。
今天就带大家简单了解一下什么是MVC,以及MVC在实际开发中是怎么使用的。
什么是MVC
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码。
将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
具体来说,M、V、C的功能分别如下:
- 视图(view)是用户看到并与之交互的界面。
- 模型(model)表示数据模型,并提供数据给视图。
- 控制器(controller)是连接视图和模型桥梁,处理业务逻辑操作,具体是指接受用户的输入并调用模型和视图去完成用户的需求。
三者之间的数据关系:
- 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中包括:
- 属性有data——存放在localstorage中的变量。
- 方法——增、删、改、查(这里写的是通用的模块,虽然本功能不涉及增、删和查)。
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中包括:
- 属性el——用于接收更新的节点容器
- 属性html——用于存放待添加的节点
- 方法init——初始化容器
- 方法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中包括:
- 属性events——多个点击事件用哈希表来展示
- 方法init——初始化容器后,再自动绑定事件,重新渲染
- 方法加、减、乘、除——触发点击事件后将m中的数据进行更新
- 方法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相关的知识了。
模块化
这里再对模块化进行一个补充吧。
今天我们做的上面这些优化,都算是模块化的一种表现。
那什么是模块呢?
模块的定义:
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的优点:
- 多人协作互不干扰
模块化避免了变量污染,并且可以使得分工更加容易 - 灵活架构,焦点分离
可以将独立的功能从主干中分离开来单独开发,增加效率 - 方便模块间组合、分解 、解耦
降低各个功能模块间的耦合度,方便维护和管理 - 方便单个模块功能调试、升级
相关规范:
- commonJS
- commonJS规范 NodeJS
- AMD
- AMD规范 requireJS
- CMD
- CMD规范 seaJs
总结
希望通过这篇文章能给大家对理解MVC和模块化有一定的帮助,如有描述不周的地方可以私聊我,欢迎交流~~~