如何用純JavaScript擼一個(gè)MVC程序,相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。

我想用 model-view-controller 架構(gòu)模式在純 JavaScript 中寫(xiě)一個(gè)簡(jiǎn)單的程序,于是我這樣做了。希望它可以幫你理解 MVC,因?yàn)楫?dāng)你剛開(kāi)始接觸它時(shí),它是一個(gè)難以理解的概念。
我做了
這個(gè)todo應(yīng)用程序,這是一個(gè)簡(jiǎn)單小巧的瀏覽器應(yīng)用,允許你對(duì)待辦事項(xiàng)進(jìn)行CRUD(創(chuàng)建,讀取,更新和刪除)操作。它只包含
index.html、style.css和script.js 三個(gè)文件,非常簡(jiǎn)單,無(wú)需任何依賴(lài)和框架。
基本的 JavaScript 和 HTML 知識(shí)
熟悉 最新的 JavaScript 語(yǔ)法
用純 JavaScript 在瀏覽器中創(chuàng)建一個(gè) todo 應(yīng)用程序,并熟悉MVC(和 OOP——面向?qū)ο缶幊蹋┑母拍睢?/p>
查看程序的演示
查看程序的源代碼
注意:由于此程序使用了最新的 JavaScript 功能(ES2017),因此在某些瀏覽器(如 Safari)上無(wú)法用 Babel 編譯為向后兼容的 JavaScript 語(yǔ)法。
MVC 是一種非常受歡迎組織代碼的模式。
Model(模型) - 管理程序的數(shù)據(jù)
View(視圖) - 模型的直觀表示
Controller(控制器) - 鏈接用戶(hù)和系統(tǒng)
模型是數(shù)據(jù)。在這個(gè) todo 程序中,這將是實(shí)際的待辦事項(xiàng),以及將添加、編輯或刪除它們的方法。
視圖是數(shù)據(jù)的顯示方式。在這個(gè)程序中,是 DOM 和 CSS 中呈現(xiàn)的 HTML。
控制器用來(lái)連接模型和視圖。它需要用戶(hù)輸入,例如單擊或鍵入,并處理用戶(hù)交互的回調(diào)。
模型永遠(yuǎn)不會(huì)觸及視圖。視圖永遠(yuǎn)不會(huì)觸及模型。控制器用來(lái)連接它們。
我想提一下,為一個(gè)簡(jiǎn)單的 todo 程序做 MVC 實(shí)際上是一大堆樣板。如果這是你想要?jiǎng)?chuàng)建的程序并且創(chuàng)建了整個(gè)系統(tǒng),那真的會(huì)讓事情變得過(guò)于復(fù)雜。關(guān)鍵是要嘗試在較小的層面上理解它。
這將是一個(gè)完全用 JavaScript 寫(xiě)的程序,這意味著一切都將通過(guò) JavaScript 處理,HTML 將只包含根元素。
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>Todo App</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div id="root"></div> <script src="script.js"></script> </body></html>
我寫(xiě)了一小部分 CSS 只是為了讓它看起來(lái)可以接受,你可以找到
這個(gè)文件并保存到
style.css 。我不打算再寫(xiě)CSS了,因?yàn)樗皇潜疚牡闹攸c(diǎn)。
好的,現(xiàn)在我們有了HTML和CSS,下面該開(kāi)始編寫(xiě)程序了。
我會(huì)使這個(gè)教程簡(jiǎn)單易懂,使你輕松了解哪個(gè)類(lèi)屬于 MVC 的哪個(gè)部分。我將創(chuàng)建一個(gè)
Model 類(lèi),View 類(lèi)和
Controller 類(lèi)。該程序?qū)⑹强刂破鞯膶?shí)例。
如果你不熟悉類(lèi)的工作方式,請(qǐng)閱讀 了解JavaScript中的類(lèi)。
class Model {
constructor() {}
}
class View {
constructor() {}
}
class Controller {
constructor(model, view) {
this.model = model
this.view = view
}
}
const app = new Controller(new Model(), new View())讓我們先關(guān)注模型,因?yàn)樗侨齻€(gè)部分中最簡(jiǎn)單的一個(gè)。它不涉及任何事件或 DOM 操作。它只是存儲(chǔ)和修改數(shù)據(jù)。
//模型
class Model {
constructor() {
// The state of the model, an array of todo objects, prepopulated with some data
this.todos = [
{ id: 1, text: 'Run a marathon', complete: false },
{ id: 2, text: 'Plant a garden', complete: false },
]
}
// Append a todo to the todos array
addTodo(todo) {
this.todos = [...this.todos, todo]
}
// Map through all todos, and replace the text of the todo with the specified id
editTodo(id, updatedText) {
this.todos = this.todos.map(todo =>
todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo
)
}
// Filter a todo out of the array by id
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
}
// Flip the complete boolean on the specified todo
toggleTodo(id) {
this.todos = this.todos.map(todo =>
todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo
)
}
}我們定義了
addTodo、editTodo、deleteTodo和toggleTodo。這些都應(yīng)該是一目了然的:add 添加到數(shù)組,edit 找到 todo 的 id 進(jìn)行編輯和替換,delete 過(guò)濾數(shù)組中的todo,并切換切換
complete 布爾屬性。
由于我們?cè)跒g覽器中執(zhí)行此操作,并且可以從窗口(全局)訪問(wèn),因此你可以輕松地測(cè)試這些內(nèi)容,輸入以下內(nèi)容:
app.model.addTodo({ id: 3, text: 'Take a nap', complete: false })將向列表中添加一個(gè)待辦事項(xiàng),你可以查看
app.model.todos 的內(nèi)容。

這對(duì)于現(xiàn)在的模型來(lái)說(shuō)已經(jīng)足夠了。最后我們會(huì)將待辦事項(xiàng)存儲(chǔ)在 local storage 中,以使其成為半永久性的,但現(xiàn)在只要刷新頁(yè)面,todo 就會(huì)刷新。
我們可以看到,該模型僅處理并修改實(shí)際數(shù)據(jù)。它不理解或不知道輸入 —— 正在修改它,或輸出 —— 最終會(huì)顯示什么。
這時(shí)如果你通過(guò)控制臺(tái)手動(dòng)輸入所有操作,并在控制臺(tái)中查看輸出,就可以獲得功能完善的 CRUD 程序所需的一切。
我們將通過(guò)操縱 DOM —— 文檔對(duì)象模型來(lái)創(chuàng)建視圖。由于沒(méi)有 React 的 JSX 或模板語(yǔ)言的幫助,在普通的 JavaScript 中執(zhí)行此操作,因此它將是冗長(zhǎng)和丑陋的,但這是直接操縱 DOM 的本質(zhì)。
控制器和模型都不應(yīng)該知道關(guān)于 DOM、HTML元素、CSS 或其中任何內(nèi)容的信息。任何與之相關(guān)的內(nèi)容都應(yīng)該放在視圖中。
如果你不熟悉 DOM 或 DOM 與 HTML 源代碼之間有什么不同,請(qǐng)閱讀 DOM簡(jiǎn)介。
要做的第一件事就是創(chuàng)建輔助方法來(lái)檢索并創(chuàng)建元素。
//視圖
class View {
constructor() {}
// Create an element with an optional CSS class
createElement(tag, className) {
const element = document.createElement(tag)
if (className) element.classList.add(className)
return element
}
// Retrieve an element from the DOM
getElement(selector) {
const element = document.querySelector(selector)
return element
}
}到目前為止還挺好。接著在構(gòu)造函數(shù)中,我將為視圖設(shè)置需要的所有東西:
應(yīng)用程序的根元素 -
#root
標(biāo)題
h2
一個(gè)表單,輸入框和提交按鈕,用于添加待辦事項(xiàng) -
form,
input,
button
待辦事項(xiàng)清單 -
ul
我將在構(gòu)造函數(shù)中創(chuàng)建所有變量,以便可以輕松地引用它們。
//視圖
class View {
constructor() {
// The root element
this.app = this.getElement('#root')
// The title of the app
this.title = this.createElement('h2')
this.title.textContent = 'Todos'
// The form, with a [type="text"] input, and a submit button
this.form = this.createElement('form')
this.input = this.createElement('input')
this.input.type = 'text'
this.input.placeholder = 'Add todo'
this.input.name = 'todo'
this.submitButton = this.createElement('button')
this.submitButton.textContent = 'Submit'
// The visual representation of the todo list
this.todoList = this.createElement('ul', 'todo-list')
// Append the input and submit button to the form
this.form.append(this.input, this.submitButton)
// Append the title, form, and todo list to the app
this.app.append(this.title, this.form, this.todoList)
}
// ...
}現(xiàn)在,將設(shè)置不會(huì)被更改的視圖部分。

另外兩個(gè)小東西:輸入(new todo)值的 getter 和 resetter。
// 視圖
get todoText() {
return this.input.value
}
resetInput() {
this.input.value = ''
}現(xiàn)在所有設(shè)置都已完成。最復(fù)雜的部分是顯示待辦事項(xiàng)列表,這是每次對(duì)待辦事項(xiàng)進(jìn)行修改時(shí)將被更改的部分。
//視圖
displayTodos(todos) {
// ...
}displayTodos 方法將創(chuàng)建待辦事項(xiàng)列表所包含的
ul 和
li 并顯示它們。每次修改、添加或刪除 todo 時(shí),都會(huì)使用模型中的
todos 再次調(diào)用
displayTodos 方法,重置列表并重新顯示它們。這將使視圖與模型的狀態(tài)保持同步。
我們要做的第一件事就是每次調(diào)用時(shí)刪除所有 todo 節(jié)點(diǎn)。然后檢查是否存在待辦事項(xiàng)。如果不這樣做,我們將會(huì)得到一個(gè)空的列表消息。
// 視圖
// Delete all nodes
while (this.todoList.firstChild) {
this.todoList.removeChild(this.todoList.firstChild)
}
// Show default message
if (todos.length === 0) {
const p = this.createElement('p')
p.textContent = 'Nothing to do! Add a task?'
this.todoList.append(p)
} else {
// ...
}現(xiàn)在循環(huán)遍歷待辦事項(xiàng)并為每個(gè)現(xiàn)有待辦事項(xiàng)顯示復(fù)選框、span 和刪除按鈕。
// 視圖
else {
// Create todo item nodes for each todo in state
todos.forEach(todo => {
const li = this.createElement('li')
li.id = todo.id
// Each todo item will have a checkbox you can toggle
const checkbox = this.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.complete
// The todo item text will be in a contenteditable span
const span = this.createElement('span')
span.contentEditable = true
span.classList.add('editable')
// If the todo is complete, it will have a strikethrough
if (todo.complete) {
const strike = this.createElement('s')
strike.textContent = todo.text
span.append(strike)
} else {
// Otherwise just display the text
span.textContent = todo.text
}
// The todos will also have a delete button
const deleteButton = this.createElement('button', 'delete')
deleteButton.textContent = 'Delete'
li.append(checkbox, span, deleteButton)
// Append nodes to the todo list
this.todoList.append(li)
})
}現(xiàn)在設(shè)置視圖及模型。我們只是沒(méi)有辦法連接它們,因?yàn)楝F(xiàn)在還沒(méi)有事件監(jiān)視用戶(hù)進(jìn)行輸入,也沒(méi)有處理這種事件的輸出的 handle。
控制臺(tái)仍然作為臨時(shí)控制器存在,你可以通過(guò)它添加和刪除待辦事項(xiàng)。

最后,控制器是模型(數(shù)據(jù))和視圖(用戶(hù)看到的內(nèi)容)之間的鏈接。這是我們到目前為止控制器中的內(nèi)容。
//控制器
class Controller {
constructor(model, view) {
this.model = model
this.view = view
}
}在視圖和模型之間的第一個(gè)鏈接是創(chuàng)建一個(gè)每次 todo 更改時(shí)調(diào)用
displayTodos 的方法。我們也可以在
constructor 中調(diào)用它一次,來(lái)顯示初始的 todos(如果有的話(huà))。
//控制器
class Controller {
constructor(model, view) {
this.model = model
this.view = view
// Display initial todos
this.onTodoListChanged(this.model.todos)
}
onTodoListChanged = todos => {
this.view.displayTodos(todos)
}
}控制器將在觸發(fā)后處理事件。當(dāng)你提交新的待辦事項(xiàng)、單擊刪除按鈕或單擊待辦事項(xiàng)的復(fù)選框時(shí),將觸發(fā)一個(gè)事件。視圖必須偵聽(tīng)這些事件,因?yàn)樗鼈兪且晥D的用戶(hù)輸入,它會(huì)將響應(yīng)事件所要做的工作分配給控制器。
我們將為事件創(chuàng)建 handler。首先,提交一個(gè)
handleAddTodo 事件,當(dāng)我們創(chuàng)建的待辦事項(xiàng)輸入表單被提交時(shí),可以通過(guò)按 Enter 鍵或單擊“提交”按鈕來(lái)觸發(fā)。這是一個(gè)
submit 事件。
回到視圖中,我們將
this.input.value 的 getter 作為
get todoText。要確保輸入不能為空,然后我們將創(chuàng)建帶有
id、text 并且
complete 值為 false 的 todo。將 todo 添加到模型中,然后重置輸入框。
// 控制器
// Handle submit event for adding a todo
handleAddTodo = event => {
event.preventDefault()
if (this.view.todoText) {
const todo = {
id: this.model.todos.length > 0 ? this.model.todos[this.model.todos.length - 1].id + 1 : 1,
text: this.view.todoText,
complete: false,
}
this.model.addTodo(todo)
this.view.resetInput()
}
}刪除 todo 的操作類(lèi)似。它將響應(yīng)刪除按鈕上的
click 事件。刪除按鈕的父元素是 todo
li 本身,它附有相應(yīng)的
id。我們需要將該數(shù)據(jù)發(fā)送給正確的模型方法。
// 控制器
// Handle click event for deleting a todo
handleDeleteTodo = event => {
if (event.target.className === 'delete') {
const id = parseInt(event.target.parentElement.id)
this.model.deleteTodo(id)
}
}在 JavaScript 中,當(dāng)你單擊復(fù)選框來(lái)切換它時(shí),會(huì)發(fā)出
change 事件。按照處理單擊刪除按鈕的方式處理此方法,并調(diào)用模型方法。
// 控制器
// Handle change event for toggling a todo
handleToggle = event => {
if (event.target.type === 'checkbox') {
const id = parseInt(event.target.parentElement.id)
this.model.toggleTodo(id)
}
}這些控制器方法有點(diǎn)亂 - 理想情況下它們不應(yīng)該處理任何邏輯,而是應(yīng)該簡(jiǎn)單地調(diào)用模型。
現(xiàn)在我們有了這三個(gè) handler ,但控制器仍然不知道應(yīng)該什么時(shí)候調(diào)用它們。必須把事件偵聽(tīng)器放在視圖中的 DOM 元素上。我們將回復(fù)表單上的submit 事件,以及 todo 列表上的
click 和
change事件。
在
View 中添加一個(gè)
bindEvents 方法,該方法將調(diào)用這些事件。
// 視圖
bindEvents(controller) {
this.form.addEventListener('submit', controller.handleAddTodo)
this.todoList.addEventListener('click', controller.handleDeleteTodo)
this.todoList.addEventListener('change', controller.handleToggle)
}接著把偵聽(tīng)事件的方法綁定到視圖。在
Controller 的
constructor 中,調(diào)用
bindEvents 并傳遞控制器的this 上下文。
在所有句柄事件上都用了箭頭函數(shù)。這允許我們可以用控制器的this上下文從視圖中調(diào)用它們。如果不用箭頭函數(shù),我們將不得不手動(dòng)去綁定它們,如controller.handleAddTodo.bind(this)。
// 控制器 this.view.bindEvents(this)
現(xiàn)在,當(dāng)指定的元素發(fā)生submit、click 或
change 事件時(shí),將會(huì)調(diào)用相應(yīng)的 handler。
我們還遺漏了一些東西:事件正在偵聽(tīng),handler 被調(diào)用,但是沒(méi)有任何反應(yīng)。這是因?yàn)槟P筒恢酪晥D應(yīng)該更新,并且不知道如何更新視圖。我們?cè)谝晥D上有
displayTodos 方法來(lái)解決這個(gè)問(wèn)題,但如前所述,模型和視圖不應(yīng)該彼此了解。
就像偵聽(tīng)事件一樣,模型應(yīng)該回到控制器,讓它知道發(fā)生了什么。
我們已經(jīng)在控制器上創(chuàng)建了
onTodoListChanged 方法來(lái)處理這個(gè)問(wèn)題,接下來(lái)只需讓模型知道它。我們將它綁定到模型,就像對(duì)視圖上的 handler 所做的一樣。
在模型中,為
onTodoListChanged 添加
bindEvents。
// 模型bindEvents(controller) {
this.onTodoListChanged = controller.onTodoListChanged
}在控制器中,發(fā)送
this 上下文。
// 控制器
constructor() {
// ...
this.model.bindEvents(this)
this.view.bindEvents(this)
}現(xiàn)在,在模型中的每個(gè)方法之后,你將調(diào)用
onTodoListChanged 回調(diào)。
在更復(fù)雜的程序中,可能對(duì)不同的事件有不同的回調(diào),但在這個(gè)簡(jiǎn)單的待辦事項(xiàng)程序中,我們可以在所有方法之間共享一個(gè)回調(diào)。
//模型
addTodo(todo) {
this.todos = [...this.todos, todo]
this.onTodoListChanged(this.todos)
}這時(shí)程序的大部分都已完成,所有概念都已經(jīng)演示過(guò)了。我們可以通過(guò)將數(shù)據(jù)保存在瀏覽器的 local storage 中來(lái)對(duì)其進(jìn)行持久化。
如果你不了解 local storage 的工作原理,請(qǐng)閱讀 如何使用JavaScript local storage。
現(xiàn)在我們可以將待辦事項(xiàng)的初始值設(shè)置為本地存儲(chǔ)或空數(shù)組。
// 模型
class Model {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || []
}
}然后創(chuàng)建一個(gè)
update 函數(shù)來(lái)更新
localStorage 的值。
//模型update() {
localStorage.setItem('todos', JSON.stringify(this.todos))
}每次更改
this.todos 后,我們都可以調(diào)用它。
//模型
addTodo(todo) {
this.todos = [...this.todos, todo]
this.update()
this.onTodoListChanged(this.todos)
}這個(gè)難題的最后一部分是編輯現(xiàn)有待辦事項(xiàng)的能力。編輯總是比添加或刪除更棘手。我想簡(jiǎn)化它,不需要編輯按鈕或用
input 或任何東西替換
span。我們也不想每輸入一個(gè)字母都調(diào)用
editTodo,因?yàn)樗鼤?huì)重新渲染整個(gè)待辦事項(xiàng)列表UI。
我決定在控制器上創(chuàng)建一個(gè)方法,用新的編輯值更新臨時(shí)狀態(tài)變量,另一個(gè)方法調(diào)用模型中的
editTodo 方法。
//控制器
constructor() {
// ...
this.temporaryEditValue
}
// Update temporary state
handleEditTodo = event => {
if (event.target.className === 'editable') {
this.temporaryEditValue = event.target.innerText
}
}
// Send the completed value to the model
handleEditTodoComplete = event => {
if (this.temporaryEditValue) {
const id = parseInt(event.target.parentElement.id)
this.model.editTodo(id, this.temporaryEditValue)
this.temporaryEditValue = ''
}
}我承認(rèn)這個(gè)解決方案有點(diǎn)亂,因?yàn)?
temporaryEditValue變量在技術(shù)上應(yīng)該在視圖中而不是在控制器中,因?yàn)樗桥c視圖相關(guān)的狀態(tài)。
現(xiàn)在我們可以將這些添加到視圖的事件偵聽(tīng)器中。當(dāng)你在
contenteditable 元素輸入時(shí),input 事件會(huì)被觸發(fā),離開(kāi)contenteditable元素時(shí),focusout 會(huì)觸發(fā)。
//視圖
bindEvents(controller) {
this.form.addEventListener('submit', controller.handleAddTodo)
this.todoList.addEventListener('click', controller.handleDeleteTodo)
this.todoList.addEventListener('input', controller.handleEditTodo)
this.todoList.addEventListener('focusout', controller.handleEditTodoComplete)
this.todoList.addEventListener('change', controller.handleToggle)
}現(xiàn)在,當(dāng)你單擊任何待辦事項(xiàng)時(shí),將進(jìn)入“編輯”模式,這將會(huì)更新臨時(shí)狀態(tài)變量,當(dāng)選中或單擊待辦事項(xiàng)時(shí),將會(huì)保存在模型中并重置臨時(shí)狀態(tài)。
contenteditable解決方案很快得到實(shí)施。在程序中使用contenteditable時(shí)需要考慮各種問(wèn)題, 我在這里寫(xiě)過(guò)許多內(nèi)容。
現(xiàn)在你擁有了一個(gè)用純 JavaScript 寫(xiě)的 todo 程序,它演示了模型 - 視圖 - 控制器體系結(jié)構(gòu)的概念。
我希望本教程能幫你理解 MVC。使用這種松散耦合的模式可以為程序添加大量的樣板和抽象,同時(shí)它也是一種開(kāi)發(fā)人員熟悉的模式,是一個(gè)通常用于許多框架的重要概念。
看完上述內(nèi)容,你們掌握如何用純JavaScript擼一個(gè)MVC程序的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司行業(yè)資訊頻道,感謝各位的閱讀!
文章標(biāo)題:如何用純JavaScript擼一個(gè)MVC程序-創(chuàng)新互聯(lián)
鏈接URL:http://chinadenli.net/article4/dpgsoe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供關(guān)鍵詞優(yōu)化、網(wǎng)頁(yè)設(shè)計(jì)公司、響應(yīng)式網(wǎng)站、用戶(hù)體驗(yàn)、服務(wù)器托管、自適應(yīng)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容