溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何用純JavaScript擼一個MVC程序

發(fā)布時間:2021-09-30 14:59:46 來源:億速云 閱讀:120 作者:柒染 欄目:web開發(fā)

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

我想用 model-view-controller 架構(gòu)模式在純 JavaScript 中寫一個簡單的程序,于是我這樣做了。希望它可以幫你理解 MVC,因為當(dāng)你剛開始接觸它時,它是一個難以理解的概念。

我做了 這個todo應(yīng)用程序,這是一個簡單小巧的瀏覽器應(yīng)用,允許你對待辦事項進行CRUD(創(chuàng)建,讀取,更新和刪除)操作。它只包含 index.htmlstyle.cssscript.js 三個文件,非常簡單,無需任何依賴和框架。

先決條件

  • 基本的 JavaScript 和 HTML 知識

  • 熟悉 最新的 JavaScript 語法

目標(biāo)

用純 JavaScript 在瀏覽器中創(chuàng)建一個 todo 應(yīng)用程序,并熟悉MVC(和 OOP——面向?qū)ο缶幊蹋┑母拍睢?/p>

  • 查看程序的演示

  • 查看程序的源代碼

注意:由于此程序使用了最新的 JavaScript 功能(ES2017),因此在某些瀏覽器(如 Safari)上無法用 Babel 編譯為向后兼容的 JavaScript 語法。

什么是 MVC?

MVC 是一種非常受歡迎組織代碼的模式。

  • Model(模型) - 管理程序的數(shù)據(jù)

  • View(視圖) - 模型的直觀表示

  • Controller(控制器)  - 鏈接用戶和系統(tǒng)

模型是數(shù)據(jù)。在這個 todo 程序中,這將是實際的待辦事項,以及將添加、編輯或刪除它們的方法。

視圖是數(shù)據(jù)的顯示方式。在這個程序中,是 DOM 和 CSS 中呈現(xiàn)的 HTML。

控制器用來連接模型和視圖。它需要用戶輸入,例如單擊或鍵入,并處理用戶交互的回調(diào)。

模型永遠(yuǎn)不會觸及視圖。視圖永遠(yuǎn)不會觸及模型。控制器用來連接它們。

我想提一下,為一個簡單的 todo 程序做 MVC 實際上是一大堆樣板。如果這是你想要創(chuàng)建的程序并且創(chuàng)建了整個系統(tǒng),那真的會讓事情變得過于復(fù)雜。關(guān)鍵是要嘗試在較小的層面上理解它。

初始設(shè)置

這將是一個完全用 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了,因為它不是本文的重點。

好的,現(xiàn)在我們有了HTML和CSS,下面該開始編寫程序了。

入門

我會使這個教程簡單易懂,使你輕松了解哪個類屬于 MVC 的哪個部分。我將創(chuàng)建一個 Model 類,View 類和 Controller 類。該程序?qū)⑹强刂破鞯膶嵗?/p>

如果你不熟悉類的工作方式,請閱讀 了解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())

模型

讓我們先關(guān)注模型,因為它是三個部分中最簡單的一個。它不涉及任何事件或 DOM 操作。它只是存儲和修改數(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、editTododeleteTodotoggleTodo。這些都應(yīng)該是一目了然的:add 添加到數(shù)組,edit 找到 todo 的 id 進行編輯和替換,delete 過濾數(shù)組中的todo,并切換切換 complete 布爾屬性。

由于我們在瀏覽器中執(zhí)行此操作,并且可以從窗口(全局)訪問,因此你可以輕松地測試這些內(nèi)容,輸入以下內(nèi)容:

app.model.addTodo({ id: 3, text: 'Take a nap', complete: false })

將向列表中添加一個待辦事項,你可以查看 app.model.todos 的內(nèi)容。

如何用純JavaScript擼一個MVC程序

這對于現(xiàn)在的模型來說已經(jīng)足夠了。最后我們會將待辦事項存儲在 local storage 中,以使其成為半永久性的,但現(xiàn)在只要刷新頁面,todo 就會刷新。

我們可以看到,該模型僅處理并修改實際數(shù)據(jù)。它不理解或不知道輸入  —— 正在修改它,或輸出  —— 最終會顯示什么。

這時如果你通過控制臺手動輸入所有操作,并在控制臺中查看輸出,就可以獲得功能完善的 CRUD 程序所需的一切。

視圖

我們將通過操縱 DOM  —— 文檔對象模型來創(chuàng)建視圖。由于沒有 React 的 JSX 或模板語言的幫助,在普通的 JavaScript 中執(zhí)行此操作,因此它將是冗長和丑陋的,但這是直接操縱 DOM 的本質(zhì)。

控制器和模型都不應(yīng)該知道關(guān)于 DOM、HTML元素、CSS 或其中任何內(nèi)容的信息。任何與之相關(guān)的內(nèi)容都應(yīng)該放在視圖中。

如果你不熟悉 DOM 或 DOM 與 HTML 源代碼之間有什么不同,請閱讀 DOM簡介。

要做的第一件事就是創(chuàng)建輔助方法來檢索并創(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

  • 一個表單,輸入框和提交按鈕,用于添加待辦事項 - form, input, button

  • 待辦事項清單 - 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è)置不會被更改的視圖部分。

如何用純JavaScript擼一個MVC程序

另外兩個小東西:輸入(new todo)值的 getter 和 resetter。

// 視圖
get todoText() {
  return this.input.value
}
resetInput() {
  this.input.value = ''
}

現(xiàn)在所有設(shè)置都已完成。最復(fù)雜的部分是顯示待辦事項列表,這是每次對待辦事項進行修改時將被更改的部分。

//視圖
displayTodos(todos) {
  // ...
}

displayTodos 方法將創(chuàng)建待辦事項列表所包含的 ulli 并顯示它們。每次修改、添加或刪除 todo 時,都會使用模型中的 todos 再次調(diào)用 displayTodos 方法,重置列表并重新顯示它們。這將使視圖與模型的狀態(tài)保持同步。

我們要做的第一件事就是每次調(diào)用時刪除所有 todo 節(jié)點。然后檢查是否存在待辦事項。如果不這樣做,我們將會得到一個空的列表消息。

// 視圖
// 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àn)有待辦事項顯示復(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è)置視圖及模型。我們只是沒有辦法連接它們,因為現(xiàn)在還沒有事件監(jiān)視用戶進行輸入,也沒有處理這種事件的輸出的 handle。

控制臺仍然作為臨時控制器存在,你可以通過它添加和刪除待辦事項。

如何用純JavaScript擼一個MVC程序

控制器

最后,控制器是模型(數(shù)據(jù))和視圖(用戶看到的內(nèi)容)之間的鏈接。這是我們到目前為止控制器中的內(nèi)容。

//控制器
class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view
  }
}

在視圖和模型之間的第一個鏈接是創(chuàng)建一個每次 todo 更改時調(diào)用 displayTodos 的方法。我們也可以在 constructor 中調(diào)用它一次,來顯示初始的 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)
  }
}

控制器將在觸發(fā)后處理事件。當(dāng)你提交新的待辦事項、單擊刪除按鈕或單擊待辦事項的復(fù)選框時,將觸發(fā)一個事件。視圖必須偵聽這些事件,因為它們是視圖的用戶輸入,它會將響應(yīng)事件所要做的工作分配給控制器。

我們將為事件創(chuàng)建 handler。首先,提交一個 handleAddTodo 事件,當(dāng)我們創(chuàng)建的待辦事項輸入表單被提交時,可以通過按 Enter 鍵或單擊“提交”按鈕來觸發(fā)。這是一個 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 的操作類似。它將響應(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ù)選框來切換它時,會發(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)
  }
}
這些控制器方法有點亂 - 理想情況下它們不應(yīng)該處理任何邏輯,而是應(yīng)該簡單地調(diào)用模型。

設(shè)置事件監(jiān)聽器

現(xiàn)在我們有了這三個 handler ,但控制器仍然不知道應(yīng)該什么時候調(diào)用它們。必須把事件偵聽器放在視圖中的 DOM 元素上。我們將回復(fù)表單上的submit 事件,以及 todo 列表上的 clickchange事件。

View 中添加一個 bindEvents 方法,該方法將調(diào)用這些事件。

// 視圖
bindEvents(controller) {
  this.form.addEventListener('submit', controller.handleAddTodo)
  this.todoList.addEventListener('click', controller.handleDeleteTodo)
  this.todoList.addEventListener('change', controller.handleToggle)
}

接著把偵聽事件的方法綁定到視圖。在 Controllerconstructor 中,調(diào)用 bindEvents 并傳遞控制器的this 上下文。

在所有句柄事件上都用了箭頭函數(shù)。這允許我們可以用控制器的 this 上下文從視圖中調(diào)用它們。如果不用箭頭函數(shù),我們將不得不手動去綁定它們,如 controller.handleAddTodo.bind(this)
// 控制器
this.view.bindEvents(this)

現(xiàn)在,當(dāng)指定的元素發(fā)生submit、clickchange 事件時,將會調(diào)用相應(yīng)的 handler。

響應(yīng)模型中的回調(diào)

我們還遺漏了一些東西:事件正在偵聽,handler 被調(diào)用,但是沒有任何反應(yīng)。這是因為模型不知道視圖應(yīng)該更新,并且不知道如何更新視圖。我們在視圖上有 displayTodos 方法來解決這個問題,但如前所述,模型和視圖不應(yīng)該彼此了解。

就像偵聽事件一樣,模型應(yīng)該回到控制器,讓它知道發(fā)生了什么。

我們已經(jīng)在控制器上創(chuàng)建了 onTodoListChanged 方法來處理這個問題,接下來只需讓模型知道它。我們將它綁定到模型,就像對視圖上的 handler 所做的一樣。

在模型中,為 onTodoListChanged 添加 bindEvents。

// 模型bindEvents(controller) {  
this.onTodoListChanged = controller.onTodoListChanged
}

在控制器中,發(fā)送 this 上下文。

// 控制器
constructor() { 
    // ...
  this.model.bindEvents(this)  
  this.view.bindEvents(this)
}

現(xiàn)在,在模型中的每個方法之后,你將調(diào)用 onTodoListChanged 回調(diào)。

在更復(fù)雜的程序中,可能對不同的事件有不同的回調(diào),但在這個簡單的待辦事項程序中,我們可以在所有方法之間共享一個回調(diào)。
//模型
addTodo(todo) {
  this.todos = [...this.todos, todo]
  this.onTodoListChanged(this.todos)
}

添加 local storage

這時程序的大部分都已完成,所有概念都已經(jīng)演示過了。我們可以通過將數(shù)據(jù)保存在瀏覽器的 local storage 中來對其進行持久化。

如果你不了解 local storage 的工作原理,請閱讀 如何使用JavaScript local storage。

現(xiàn)在我們可以將待辦事項的初始值設(shè)置為本地存儲或空數(shù)組。

// 模型
class Model {
  constructor() {
    this.todos = JSON.parse(localStorage.getItem('todos')) || []
  }
}

然后創(chuàng)建一個 update 函數(shù)來更新 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)
}

添加實時編輯功能

這個難題的最后一部分是編輯現(xiàn)有待辦事項的能力。編輯總是比添加或刪除更棘手。我想簡化它,不需要編輯按鈕或用 input 或任何東西替換 span。我們也不想每輸入一個字母都調(diào)用 editTodo,因為它會重新渲染整個待辦事項列表UI。

我決定在控制器上創(chuàng)建一個方法,用新的編輯值更新臨時狀態(tài)變量,另一個方法調(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)這個解決方案有點亂,因為 temporaryEditValue 變量在技術(shù)上應(yīng)該在視圖中而不是在控制器中,因為它是與視圖相關(guān)的狀態(tài)。

現(xiàn)在我們可以將這些添加到視圖的事件偵聽器中。當(dāng)你在 contenteditable 元素輸入時,input 事件會被觸發(fā),離開contenteditable元素時,focusout 會觸發(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)你單擊任何待辦事項時,將進入“編輯”模式,這將會更新臨時狀態(tài)變量,當(dāng)選中或單擊待辦事項時,將會保存在模型中并重置臨時狀態(tài)。

contenteditable 解決方案很快得到實施。在程序中使用 contenteditable 時需要考慮各種問題, 我在這里寫過許多內(nèi)容。

現(xiàn)在你擁有了一個用純 JavaScript 寫的 todo 程序,它演示了模型 - 視圖 - 控制器體系結(jié)構(gòu)的概念。

我希望本教程能幫你理解 MVC。使用這種松散耦合的模式可以為程序添加大量的樣板和抽象,同時它也是一種開發(fā)人員熟悉的模式,是一個通常用于許多框架的重要概念。

看完上述內(nèi)容,你們掌握如何用純JavaScript擼一個MVC程序的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI