參考: 自制前端框架之 MVC
參考: MVC,MVP 和 MVVM 的圖示
如何設(shè)計(jì)一個(gè)程序的結(jié)構(gòu),這是一門(mén)專門(mén)的學(xué)問(wèn)肮帐,叫做"架構(gòu)模式"(architectural pattern)纷责,屬于編程的方法論。MVC 模式就是架構(gòu)模式的一種臣樱,在 UI 編程領(lǐng)域大有大量使用 MVC 模式的開(kāi)發(fā)框架 (Django等后端框架)靶擦,使得開(kāi)發(fā)者能夠借助該模式腮考,構(gòu)建出更易于擴(kuò)展和維護(hù)的應(yīng)用程序。
MVC 簡(jiǎn)介
大體上可將 MVC 模式的結(jié)構(gòu)分為三層玄捕,即 Model(模型)踩蔚、View(視圖)和Controller(控制)
- Model: 專注數(shù)據(jù)的存取,對(duì)基礎(chǔ)數(shù)據(jù)對(duì)象的封裝枚粘。
- Controller: 處理具體業(yè)務(wù)邏輯馅闽,即根據(jù)用戶從"視圖層"輸入的指令,從 Model 中存取數(shù)據(jù)馍迄,并渲染到 View 中福也。
- View : 負(fù)責(zé)界面視圖,供用戶查看和操作攀圈,可理解為【輸入數(shù)據(jù)暴凑,輸出界面】的模塊,在其中通常不涉及的業(yè)務(wù)邏輯量承。
這三層是緊密聯(lián)系在一起的搬设,但又是互相獨(dú)立的,每一層內(nèi)部的變化不影響其他層撕捍。每一層都對(duì)外提供接口(Interface)拿穴,供其他層調(diào)用。這樣一來(lái)忧风,軟件就可以實(shí)現(xiàn)模塊化默色,修改外觀或者變更數(shù)據(jù)都不用修改其他層,大大方便了維護(hù)和升級(jí)狮腿。
需要注意的是腿宰,MVC 僅是一種模式理念,而非具體的規(guī)范缘厢。因此吃度,根據(jù) MVC 的理念所設(shè)計(jì)出的框架,在實(shí)現(xiàn)和使用上可能存在著較大的區(qū)別贴硫。
咱們的目標(biāo)
常見(jiàn)的后端框架所封裝的功能椿每,不外乎對(duì)數(shù)據(jù)的增查改刪與渲染。在前端英遭,我們以一個(gè)非常簡(jiǎn)單的 Todo App 作為示例间护,來(lái)實(shí)際看看 MVC 模式到底是怎樣工作的。
- Model 模塊實(shí)現(xiàn) Todo 這一數(shù)據(jù)模型的存取挖诸。
- View 模塊實(shí)現(xiàn)將 Todo 數(shù)據(jù)模型渲染到頁(yè)面汁尺。
- Controller 模塊實(shí)現(xiàn)對(duì) Todo 數(shù)據(jù)的新增、編輯多律、刪除等操作痴突。
編寫(xiě)代碼
Model 模塊
按照 MVC 模式搂蜓,Model 模塊的主要工作是存取數(shù)據(jù),并且在數(shù)據(jù)變化時(shí)將新數(shù)據(jù)傳給 View 模塊苞也。Model 模塊的核心是訂閱-發(fā)布者模式
洛勉。
class Model {
// 在構(gòu)造器中實(shí)例化數(shù)據(jù)與訂閱者
// 本例中的數(shù)據(jù)就是 一個(gè)個(gè)的 todo
constructor() {
// 數(shù)據(jù)格式 [{id: 1, value: '123'}]
this.todo = []
this.todo
// 【初始化訂閱者】
this.subscribers = []
}
// 利用 ES6 class 語(yǔ)法定義模型實(shí)例的 getter
// 從而在調(diào)用 model.data 時(shí)返回正確的 Todo 數(shù)據(jù)
get data() {
return this.todo
}
// 利用 ES6 class 語(yǔ)法定義模型實(shí)例的 setter
// 從而在執(zhí)行形如 modle.data = newData 的賦值時(shí)
// 能夠通知訂閱了 Model 的模塊進(jìn)行相應(yīng)更新 【數(shù)據(jù)更新時(shí),觸發(fā)訂閱回調(diào)】
set data(data) {
this.todo = data
this.publish(this.todo)
}
// 由 Model 實(shí)例調(diào)用的發(fā)布方法
// 在 Model 中的 setter 更新時(shí)如迟,將新數(shù)據(jù)傳入該方法
// 由該方法將新數(shù)據(jù)推送到每個(gè)訂閱者提供的回調(diào)中
// 在 本項(xiàng)目中,訂閱者為 Controller 的 render 方法 【觸發(fā)所有訂閱】
publish(data) {
// 此處的訂閱者是 業(yè)務(wù)中的函數(shù)
this.subscribers.forEach(render => render(data))
}
}
在示例中可以發(fā)現(xiàn)攻走,所謂的發(fā)布 - 訂閱模式殷勘,其思路和實(shí)現(xiàn)均非常簡(jiǎn)單:
- 區(qū)分出【發(fā)布者】和【訂閱者】的概念。本例中 Model 為發(fā)布者昔搂,Controller 為訂閱者玲销。
- 在發(fā)布者中維護(hù)【我有哪些訂閱者】信息的數(shù)組,每個(gè)元素為一個(gè)訂閱者提供的回調(diào)摘符。
- 發(fā)布者數(shù)據(jù)更新時(shí)贤斜,依次觸發(fā)所有訂閱者的回調(diào)。
不過(guò)逛裤,Model 中的代碼僅實(shí)現(xiàn)了【初始化發(fā)布者】與【觸發(fā)所有訂閱】,【數(shù)據(jù)更新時(shí)瘩绒,觸發(fā)訂閱回調(diào)】的功能,并不是一個(gè)完整的發(fā)布 - 訂閱模式带族。在完整的模式實(shí)現(xiàn)中锁荔,其余代碼包括:
- 【訂閱者訂閱發(fā)布者】機(jī)制的實(shí)現(xiàn),其代碼位置為
Controller
中的最后一行this.model.subscribers.push(this.render)
蝙砌,在此將 render 方法作為訂閱者回調(diào)阳堕,提供給了發(fā)布者。 - 【訂閱者提供的訂閱方法】的實(shí)現(xiàn)择克,在此即為 Controller 中提供的
this.render
方法恬总。
Controller 模塊
上文中已經(jīng)明確,Controller 模塊需要實(shí)現(xiàn)的功能為:
- 與 Model / View 實(shí)例的綁定肚邢。
- 對(duì)點(diǎn)擊事件壹堰、DOM 選擇等底層 API 的封裝。
- 用于渲染數(shù)據(jù)的 Render 方法道偷。
class Controller {
constructor(conf) {
// 根據(jù)實(shí)例化參數(shù)缀旁,定義 Controller 基礎(chǔ)配置
// 包括 DOM 容器、Model / View 實(shí)例及 onClick 事件等
this.el = document.querySelector(conf.el)
this.model = conf.model
this.view = conf.view
// 為 容器 dom 設(shè)置事件
this.bindEvent(this.el)
// 給 render 函數(shù)綁定 this
this.render = this.render.bind(this)
// 在 Model 更新時(shí)執(zhí)行 controller 的 render 方法
this.model.subscribers.push(this.render)
}
// 根據(jù)點(diǎn)擊 btn 的 class 屬性綁定不同的事件回調(diào)
bindEvent() {
this.el.addEventListener('click', (event) => {
let el = event.target
let id = el.dataset.id
if (el.classList.contains('todo-delete')) {
deleteTodo(id)
} else if (el.classList.contains('todo-update')) {
updateTodo(el, id)
} else if (el.classList.contains('todo-add')) {
addTodo(el)
}
})
// 點(diǎn)擊 add 時(shí)勺鸦,把新的 todo 添加到 model 中
const addTodo = (el) => {
let input = el.parentElement.querySelector('.input-add')
let value = input.value
if (value !== '') {
let data = this.model.data
data.push({
id: data.length,
value: value
})
this.model.data = data
}
}
// 點(diǎn)擊 delete 時(shí)并巍,把對(duì)應(yīng) todo 從 model 中刪除
const deleteTodo = (id) => {
this.model.data = this.model.data.filter((todo) => {
return id !== String(todo.id)
})
}
// 點(diǎn)擊 update 時(shí),把對(duì)應(yīng) todo 在 model 中更新
const updateTodo = (el, id) => {
this.model.data = this.model.data.map((todo) => {
return ({
id: todo.id,
value: setValue(id, todo)
})
})
}
// 輔助 更新函數(shù)
const setValue = (id, todo) => {
if (id === String(todo.id)) {
let updateInput = document.querySelector(`input[data-id="${todo.id}"]`)
return updateInput.value
} else {
return todo.value
}
}
}
// 全量重置 DOM 的 naive render 實(shí)現(xiàn)
render() {
// 由于 view 是純函數(shù)换途,故而直接對(duì)其傳入 Model 數(shù)據(jù)
// 將輸出的 HTML 模板作為 Controller DOM 內(nèi)的新?tīng)顟B(tài)
this.el.innerHTML = this.view(this.model.todo)
}
}
可以看到 Controller 模塊在點(diǎn)擊事件觸發(fā)時(shí)懊渡,沒(méi)有直接修改 dom, 只是修改了 model 中的數(shù)據(jù)刽射, dom 視圖的改變是在 model 中數(shù)據(jù)變化時(shí)自動(dòng) render 的。
View 模塊
如前文所述剃执, View 模塊實(shí)質(zhì)上就是一個(gè)在 Model 中數(shù)據(jù)變更時(shí)誓禁,由 Controller 在 render 方法中執(zhí)行的一個(gè)純函數(shù)。
function view(todos) {
const todosList = todos.map(todo => `
<div>
<span data-id="${todo.id}">
${todo.value}
</span>
<button data-id="${todo.id}" class="todo-delete">
刪除
</button>
<span>
<input data-id="${todo.id}"/>
<button data-id="${todo.id}" class="todo-update">
Update
</button>
</span>
</div>
`).join('')
return (`
<main>
<input class="input-add"/>
<button class="todo-add">Add</button>
<div>${todosList}</div>
</main>
`)
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./mvc.js"></script>
<script>
const model = new Model()
let conf = {
model,
view,
el: '#app',
}
const controller = new Controller(conf)
controller.render()
</script>
</body>
</html>
總結(jié)
在實(shí)現(xiàn) MVC 模式的 todo app 的過(guò)程中肾档,MVC 模式的特性得到了體現(xiàn),【Model, View】模塊直接只對(duì)外提供接口摹恰, 【Controller】模塊則將兩者連接了起來(lái),而 ES6 所提供的 class 高級(jí)特性則大大簡(jiǎn)化這些特性的實(shí)現(xiàn)復(fù)雜度(setter getter 等)怒见。
最后的最后俗慈,咱們?cè)偈崂硐铝鞒蹋?/p>
點(diǎn)擊按鈕 -> 觸發(fā) controller 里的事件 -> 更改 model 數(shù)據(jù) -> 觸發(fā) render 函數(shù) -> 更新視圖