作者:Tania翻譯:瘋狂的技術宅
原文:https://www.taniarascia.com/j...
未經允許嚴禁轉載
我想用 model-view-controller 架構模式在純 JavaScript 中寫一個簡單的程式,於是我這樣做了。希望它可以幫你理解 MVC,因為當你剛開始接觸它時,它是一個難以理解的概念。
我做了這個todo應用程式,這是一個簡單小巧的瀏覽器應用,允許你對待辦事項進行CRUD(建立,讀取,更新和刪除)操作。它只包含 index.html
、style.css
和script.js
三個檔案,非常簡單,無需任何依賴和框架。
先決條件
- 基本的 JavaScript 和 HTML 知識
- 熟悉最新的 JavaScript 語法
目標
用純 JavaScript 在瀏覽器中建立一個 todo 應用程式,並熟悉MVC(和 OOP——物件導向程式設計)的概念。
注意:由於此程式使用了最新的 JavaScript 功能(ES2017),因此在某些瀏覽器(如 Safari)上無法用 Babel 編譯為向後相容的 JavaScript 語法。
什麼是 MVC?
MVC 是一種非常受歡迎組織程式碼的模式。
- Model(模型) - 管理程式的資料
- View(檢視) - 模型的直觀表示
- Controller(控制器) - 連結使用者和系統
模型是資料。在這個 todo 程式中,這將是實際的待辦事項,以及將新增、編輯或刪除它們的方法。
檢視是資料的顯示方式。在這個程式中,是 DOM 和 CSS 中呈現的 HTML。
控制器用來連線模型和檢視。它需要使用者輸入,例如單擊或鍵入,並處理使用者互動的回撥。
模型永遠不會觸及檢視。檢視永遠不會觸及模型。控制器用來連線它們。
我想提一下,為一個簡單的 todo 程式做 MVC 實際上是一大堆樣板。如果這是你想要建立的程式並且建立了整個系統,那真的會讓事情變得過於複雜。關鍵是要嘗試在較小的層面上理解它。
初始設定
這將是一個完全用 JavaScript 寫的程式,這意味著一切都將通過 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>
我寫了一小部分 CSS 只是為了讓它看起來可以接受,你可以找到這個檔案並儲存到 style.css
。我不打算再寫CSS了,因為它不是本文的重點。
好的,現在我們有了HTML和CSS,下面該開始編寫程式了。
入門
我會使這個教程簡單易懂,使你輕鬆瞭解哪個類屬於 MVC 的哪個部分。我將建立一個 Model
類,View
類和 Controller
類。該程式將是控制器的例項。
如果你不熟悉類的工作方式,請閱讀瞭解JavaScript中的類。
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())
模型
讓我們先關注模型,因為它是三個部分中最簡單的一個。它不涉及任何事件或 DOM 操作。它只是儲存和修改資料。
//模型
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
。這些都應該是一目瞭然的:add 新增到陣列,edit 找到 todo 的 id 進行編輯和替換,delete 過濾陣列中的todo,並切換切換 complete
布林屬性。
由於我們在瀏覽器中執行此操作,並且可以從視窗(全域性)訪問,因此你可以輕鬆地測試這些內容,輸入以下內容:
app.model.addTodo({ id: 3, text: 'Take a nap', complete: false })
將向列表中新增一個待辦事項,你可以檢視 app.model.todos
的內容。
這對於現在的模型來說已經足夠了。最後我們會將待辦事項儲存在 local storage 中,以使其成為半永久性的,但現在只要重新整理頁面,todo 就會重新整理。
我們可以看到,該模型僅處理並修改實際資料。它不理解或不知道輸入 —— 正在修改它,或輸出 —— 最終會顯示什麼。
這時如果你通過控制檯手動輸入所有操作,並在控制檯中檢視輸出,就可以獲得功能完善的 CRUD 程式所需的一切。
檢視
我們將通過操縱 DOM —— 文件物件模型來建立檢視。由於沒有 React 的 JSX 或模板語言的幫助,在普通的 JavaScript 中執行此操作,因此它將是冗長和醜陋的,但這是直接操縱 DOM 的本質。
控制器和模型都不應該知道關於 DOM、HTML元素、CSS 或其中任何內容的資訊。任何與之相關的內容都應該放在檢視中。
如果你不熟悉 DOM 或 DOM 與 HTML 原始碼之間有什麼不同,請閱讀DOM簡介。
要做的第一件事就是建立輔助方法來檢索並建立元素。
//檢視
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
}
}
到目前為止還挺好。接著在建構函式中,我將為檢視設定需要的所有東西:
- 應用程式的根元素 -
#root
- 標題
h1
- 一個表單,輸入框和提交按鈕,用於新增待辦事項 -
form
,input
,button
- 待辦事項清單 -
ul
我將在建構函式中建立所有變數,以便可以輕鬆地引用它們。
//檢視
class View {
constructor() {
// The root element
this.app = this.getElement('#root')
// The title of the app
this.title = this.createElement('h1')
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)
}
// ...
}
現在,將設定不會被更改的檢視部分。
另外兩個小東西:輸入(new todo)值的 getter 和 resetter。
// 檢視
get todoText() {
return this.input.value
}
resetInput() {
this.input.value = ''
}
現在所有設定都已完成。最複雜的部分是顯示待辦事項列表,這是每次對待辦事項進行修改時將被更改的部分。
//檢視
displayTodos(todos) {
// ...
}
displayTodos
方法將建立待辦事項列表所包含的 ul
和 li
並顯示它們。每次修改、新增或刪除 todo 時,都會使用模型中的 todos
再次呼叫 displayTodos
方法,重置列表並重新顯示它們。這將使檢視與模型的狀態保持同步。
我們要做的第一件事就是每次呼叫時刪除所有 todo 節點。然後檢查是否存在待辦事項。如果不這樣做,我們將會得到一個空的列表訊息。
// 檢視
// 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 {
// ...
}
現在迴圈遍歷待辦事項併為每個現有待辦事項顯示覆選框、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)
})
}
現在設定檢視及模型。我們只是沒有辦法連線它們,因為現在還沒有事件監視使用者進行輸入,也沒有處理這種事件的輸出的 handle。
控制檯仍然作為臨時控制器存在,你可以通過它新增和刪除待辦事項。
控制器
最後,控制器是模型(資料)和檢視(使用者看到的內容)之間的連結。這是我們到目前為止控制器中的內容。
//控制器
class Controller {
constructor(model, view) {
this.model = model
this.view = view
}
}
在檢視和模型之間的第一個連結是建立一個每次 todo 更改時呼叫 displayTodos
的方法。我們也可以在 constructor
中呼叫它一次,來顯示初始的 todos(如果有的話)。
//控制器
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)
}
}
控制器將在觸發後處理事件。當你提交新的待辦事項、單擊刪除按鈕或單擊待辦事項的核取方塊時,將觸發一個事件。檢視必須偵聽這些事件,因為它們是檢視的使用者輸入,它會將響應事件所要做的工作分配給控制器。
我們將為事件建立 handler。首先,提交一個 handleAddTodo
事件,當我們建立的待辦事項輸入表單被提交時,可以通過按 Enter 鍵或單擊“提交”按鈕來觸發。這是一個 submit
事件。
回到檢視中,我們將 this.input.value
的 getter 作為 get todoText
。要確保輸入不能為空,然後我們將建立帶有 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 的操作類似。它將響應刪除按鈕上的 click
事件。刪除按鈕的父元素是 todo li
本身,它附有相應的 id
。我們需要將該資料傳送給正確的模型方法。
// 控制器
// 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 中,當你單擊核取方塊來切換它時,會發出 change
事件。按照處理單擊刪除按鈕的方式處理此方法,並呼叫模型方法。
// 控制器
// 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)
}
}
這些控制器方法有點亂 - 理想情況下它們不應該處理任何邏輯,而是應該簡單地呼叫模型。
設定事件監聽器
現在我們有了這三個 handler ,但控制器仍然不知道應該什麼時候呼叫它們。必須把事件偵聽器放在檢視中的 DOM 元素上。我們將回復表單上的submit
事件,以及 todo 列表上的 click
和 change
事件。
在 View
中新增一個 bindEvents
方法,該方法將呼叫這些事件。
// 檢視
bindEvents(controller) {
this.form.addEventListener('submit', controller.handleAddTodo)
this.todoList.addEventListener('click', controller.handleDeleteTodo)
this.todoList.addEventListener('change', controller.handleToggle)
}
接著把偵聽事件的方法繫結到檢視。在 Controller
的 constructor
中,呼叫 bindEvents
並傳遞控制器的this
上下文。
在所有控制程式碼事件上都用了箭頭函式。這允許我們可以用控制器的this
上下文從檢視中呼叫它們。如果不用箭頭函式,我們將不得不手動去繫結它們,如controller.handleAddTodo.bind(this)
。
// 控制器
this.view.bindEvents(this)
現在,當指定的元素髮生submit
、click
或 change
事件時,將會呼叫相應的 handler。
響應模型中的回撥
我們還遺漏了一些東西:事件正在偵聽,handler 被呼叫,但是沒有任何反應。這是因為模型不知道檢視應該更新,並且不知道如何更新檢視。我們在檢視上有 displayTodos
方法來解決這個問題,但如前所述,模型和檢視不應該彼此瞭解。
就像偵聽事件一樣,模型應該回到控制器,讓它知道發生了什麼。
我們已經在控制器上建立了 onTodoListChanged
方法來處理這個問題,接下來只需讓模型知道它。我們將它繫結到模型,就像對檢視上的 handler 所做的一樣。
在模型中,為 onTodoListChanged
新增 bindEvents
。
// 模型
bindEvents(controller) {
this.onTodoListChanged = controller.onTodoListChanged
}
在控制器中,傳送 this
上下文。
// 控制器
constructor() {
// ...
this.model.bindEvents(this)
this.view.bindEvents(this)
}
現在,在模型中的每個方法之後,你將呼叫 onTodoListChanged
回撥。
在更復雜的程式中,可能對不同的事件有不同的回撥,但在這個簡單的待辦事項程式中,我們可以在所有方法之間共享一個回撥。
//模型
addTodo(todo) {
this.todos = [...this.todos, todo]
this.onTodoListChanged(this.todos)
}
新增 local storage
這時程式的大部分都已完成,所有概念都已經演示過了。我們可以通過將資料儲存在瀏覽器的 local storage 中來對其進行持久化。
如果你不瞭解 local storage 的工作原理,請閱讀如何使用JavaScript local storage。
現在我們可以將待辦事項的初始值設定為本地儲存或空陣列。
// 模型
class Model {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || []
}
}
然後建立一個 update
函式來更新 localStorage
的值。
//模型
update() {
localStorage.setItem('todos', JSON.stringify(this.todos))
}
每次更改 this.todos
後,我們都可以呼叫它。
//模型
addTodo(todo) {
this.todos = [...this.todos, todo]
this.update()
this.onTodoListChanged(this.todos)
}
新增實時編輯功能
這個難題的最後一部分是編輯現有待辦事項的能力。編輯總是比新增或刪除更棘手。我想簡化它,不需要編輯按鈕或用 input
或任何東西替換 span
。我們也不想每輸入一個字母都呼叫 editTodo
,因為它會重新渲染整個待辦事項列表UI。
我決定在控制器上建立一個方法,用新的編輯值更新臨時狀態變數,另一個方法呼叫模型中的 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 = ''
}
}
我承認這個解決方案有點亂,因為 temporaryEditValue
變數在技術上應該在檢視中而不是在控制器中,因為它是與檢視相關的狀態。
現在我們可以將這些新增到檢視的事件偵聽器中。當你在 contenteditable
元素輸入時,input
事件會被觸發,離開contenteditable
元素時,focusout
會觸發。
//檢視
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)
}
現在,當你單擊任何待辦事項時,將進入“編輯”模式,這將會更新臨時狀態變數,當選中或單擊待辦事項時,將會儲存在模型中並重置臨時狀態。
contenteditable
解決方案很快得到實施。在程式中使用contenteditable
時需要考慮各種問題,我在這裡寫過許多內容。
總結
現在你擁有了一個用純 JavaScript 寫的 todo 程式,它演示了模型 - 檢視 - 控制器體系結構的概念。以下是演示和原始碼的連結。
我希望本教程能幫你理解 MVC。使用這種鬆散耦合的模式可以為程式新增大量的樣板和抽象,同時它也是一種開發人員熟悉的模式,是一個通常用於許多框架的重要概念。
本文首發微信公眾號:前端先鋒
歡迎掃描二維碼關注公眾號,每天都給你推送新鮮的前端技術文章
歡迎繼續閱讀本專欄其它高贊文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 實現虛擬現實遊戲
- 13個幫你提高開發效率的現代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?從呼叫棧到Promise你需要知道的一切
- WebSocket實戰:在 Node 和 React 之間進行實時通訊
- 關於 Git 的 20 個面試題
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什麼?
- 30分鐘用Node.js構建一個API伺服器
- Javascript的物件拷貝
- 程式設計師30歲前月薪達不到30K,該何去何從
- 14個最好的 JavaScript 資料視覺化庫
- 8 個給前端的頂級 VS Code 擴充套件外掛
- Node.js 多執行緒完全指南
- 把HTML轉成PDF的4個方案及實現