您好,登錄后才能下訂單哦!
如何用純JavaScript擼一個MVC程序,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
我想用 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 是一種非常受歡迎組織代碼的模式。
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
標題
h2
一個表單,輸入框和提交按鈕,用于添加待辦事項 -
form
,
input
,
button
待辦事項清單 -
ul
我將在構造函數中創建所有變量,以便可以輕松地引用它們。
//視圖 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) } // ... }
現在,將設置不會被更改的視圖部分。
另外兩個小東西:輸入(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 的工作原理,請閱讀 如何使用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。使用這種松散耦合的模式可以為程序添加大量的樣板和抽象,同時它也是一種開發人員熟悉的模式,是一個通常用于許多框架的重要概念。
看完上述內容,你們掌握如何用純JavaScript擼一個MVC程序的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。