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()。
这些方法都没有固定的形式,依照需求的内容而定,但它们的大体步骤是一样的:
- 在 JS 引擎中处理数据。
- 将结果传递给渲染引擎进行渲染。
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)
通过 .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 导入这部分代码。
模块化使得各个功能之间解耦,每个模块都不需要知道另一个模块的具体功能,甚至每个模块都可以使用不同的方法实现。
这在多人协同的大型项目中具有极大的作用,每个功能的负责程序员无需知道另一个功能是如何实现的、担心自己的代码是否会干扰到另一个模块中的代码。
另一方面,这也方便日后对代码的重构。通过模块化把各个功能区隔离开来,我们完全可以只对整体功能中的一部分进行重构,而无需对整个程序进行重构,这极大提高了代码的可维护性。
并且,通过模块化,我们可以把部分通用的代码抽取出来,放入一个原型或者类中,从而提高了代码的复用性。