JavaScript 从零开始构建一个简单的 MVC 应用程序

JavaScript 从零开始构建一个简单的 MVC 应用程序,第1张

我想使用模型-视图-控制器架构模式用纯 JavaScript 编写一个简单的应用程序。所以我做到了,就在这里。希望它可以帮助您理解 MVC,因为当您刚开始时,它是一个难以理解的概念。

我制作了这个 todo 应用程序,它是一个简单的小浏览器应用程序,允许您对 todos 进行 CRUD(创建、读取、更新和删除)。它只包含一个index.html,style.cssscript.js, 非常漂亮和简单,并且没有依赖/框架,用于学习目的。

先决条件

基本的 JavaScript 和 HTML熟悉最新的 JavaScript 语法

目标

使用纯 JavaScript 在浏览器中创建待办事项应用程序,并熟悉 MVC(和 OOP - 面向对象编程)的概念。

查看演示查看源代码

注意:由于此应用程序使用最新的 JavaScript 功能 (ES2017),因此如果不使用 Babel 编译为向后兼容的 JavaScript 语法,它将无法在 Safari 等某些浏览器上按原样运行。

什么是模型视图控制器?

MVC 是组织代码的一种可能模式。这是一个受欢迎的。

模型- 管理应用程序的数据视图- 模型的可视化表示控制器- 链接用户和系统

模型就是数据。在这个 todo 应用程序中,这将是实际的 todo,以及添加、编辑或删除它们的方法。

视图是数据的显示方式。在这个 todo 应用程序中,这将是 DOM 和 CSS 中呈现的 HTML。

控制器连接模型和视图。它接受用户输入,例如单击或键入,并处理用户交互的回调。

模型从不接触视图。视图从不接触模型。控制器将它们连接起来。

我想提一下,为一个简单的 todo 应用程序做 MVC 实际上是一大堆样板文件。如果这是您想要创建的应用程序并且您制作了整个系统,那真的会让事情变得过于复杂。关键是尝试在一个小的层面上理解它,这样你就可以理解为什么一个规模化的系统可能会使用它。

初始设置

这将是一个完整的 JavaScript 应用程序,这意味着一切都将通过 JavaScript 处理,而 HTML 将仅包含正文中的单个根元素。

index.html



  
    
    
    

    Todo App

    
  

  
    

    
  

我写了一点 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},
    ]
  }

  addTodo(todoText) {
    const todo = {
      id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1,
      text: todoText,
      complete: false,
    }

    this.todos.push(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,
    )
  }
}

我们有addTodoeditTodo,deleteTodotoggleTodo。这些都应该是非常自我解释的 - add 将一个新的 todo 附加到数组,edit 找到要编辑的 todo 的 id 并替换它,从数组中删除过滤器一个 todo,并切换complete布尔属性。

由于我们在浏览器中完成所有这些 *** 作,并且可以从窗口(全局)访问应用程序,因此您可以轻松地测试它们,输入如下内容:

app.model.addTodo('Take a nap')

将在列表中添加一个待办事项,您可以记录app.model.todos.

这对于现在的模型来说已经足够了。最后,我们会将待办事项存储在本地存储中以使其成为半永久性的,但现在待办事项只会在您刷新页面时刷新。

正如我们所看到的,模型只处理实际数据,并修改该数据。它不理解或不知道输入- 修改它的内容或输出- 最终将显示的内容。

此时,如果您通过控制台手动键入所有 *** 作,并在控制台中查看输出,您就拥有了功能齐全的 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用于添加待办事项的表单、输入和提交按钮 - forminput,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)
  }
  // ...
}

现在,视图中不会更改的部分已设置完毕。

还有两件小事——输入(新的待办事项)值的获取器和重置器。

我在方法名称中使用下划线来表示它们是私有(本地)方法,不会在类外使用。

视图
get _todoText() {
  return this.input.value
}

_resetInput() {
  this.input.value = ''
}

现在所有的设置都完成了。最复杂的部分是显示待办事项列表,这是每次对待办事项进行更改时都会更改的部分。

视图
displayTodos(todos) {
  // ...
}

displayTodos方法将创建待办事项列表所包含的ulandli并显示它们。每次更改、添加或删除待办事项时,displayTodos都会使用todos模型中的 再次调用该方法,重置列表并重新显示它们。这将使视图与模型状态保持同步。

我们要做的第一件事是在每次调用时删除所有的 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 {
  // ...
}

现在我们将遍历待办事项并为每个现有的待办事项显示一个复选框、跨度和删除按钮。

视图
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)
  })
}

现在视图已设置,模型已设置。我们只是没有办法连接它们——没有事件监视用户进行输入,也没有处理程序来处理此类事件的输出。

控制台仍然作为临时控制器存在,您可以通过它添加和删除待办事项。

控制器

最后,控制器是模型(数据)和视图(用户所见)之间的链接。这是我们迄今为止在控制器中的内容。

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

displayTodos我们在视图和模型之间的第一个链接是创建一个在每次 todo 更改时调用的方法。我们也可以在 中调用它一次constructor以显示初始待办事项(如果有的话)。

控制器
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)
  }
}

控制器将在事件被触发后处理它们。当你提交一个新的待办事项,或点击删除按钮,或点击待办事项的复选框时,将触发一个事件。视图必须侦听这些事件,因为它们是视图的用户输入,但它会将响应事件所发生的事情的责任分派给控制器。

我们将为控制器中的事件创建处理程序。

控制器
handleAddTodo = (todoText) => {
  this.model.addTodo(todoText)
}

handleEditTodo = (id, todoText) => {
  this.model.editTodo(id, todoText)
}

handleDeleteTodo = (id) => {
  this.model.deleteTodo(id)
}

handleToggleTodo = (id) => {
  this.model.toggleTodo(id)
}
设置事件监听器

现在我们有了这些处理程序,但控制器仍然不知道何时调用它们。我们必须在视图中的 DOM 元素上放置事件监听器。我们将响应submit表单上的事件click以及change待办事项列表上的事件。(我现在跳过“编辑”,因为它稍微复杂一些。)

视图
bindAddTodo(handler) {
  this.form.addEventListener('submit', event => {
    event.preventDefault()

    if (this._todoText) {
      handler(this._todoText)
      this._resetInput()
    }
  })
}

bindDeleteTodo(handler) {
  this.todoList.addEventListener('click', event => {
    if (event.target.className === 'delete') {
      const id = parseInt(event.target.parentElement.id)

      handler(id)
    }
  })
}

bindToggleTodo(handler) {
  this.todoList.addEventListener('change', event => {
    if (event.target.type === 'checkbox') {
      const id = parseInt(event.target.parentElement.id)

      handler(id)
    }
  })
}

我们需要从视图中调用处理程序,因此我们要将监听事件的方法绑定到视图。

我们在所有句柄事件上使用了箭头函数。this这允许我们使用控制器的上下文从视图中调用它们。如果我们不使用箭头函数,我们将不得不手动绑定它们,例如this.view.bindAddTodo(this.handleAddTodo.bind(this)). 哎呀。

控制器
this.view.bindAddTodo(this.handleAddTodo)
this.view.bindDeleteTodo(this.handleDeleteTodo)
this.view.bindToggleTodo(this.handleToggleTodo)
// this.view.bindEditTodo(this.handleEditTodo) - We'll do this one last

现在,当指定元素submit上发生clickchange事件时,将调用相应的处理程序。

响应模型中的回调

我们遗漏了一些东西 - 事件正在侦听,处理程序被调用,但没有任何反应。这是因为模型不知道视图应该更新,也不知道如何使视图更新。我们有displayTodos视图上的方法来解决这个问题,但是如前所述,模型和视图不应该相互了解。

就像监听事件一样,模型应该回火给控制器,让它知道发生了什么事。

我们已经onTodoListChanged在控制器上创建了方法来处理这个问题,我们只需要让模型知道它。我们将把它绑定到模型上,就像我们对视图上的处理程序所做的那样。

在模型中,添加bindTodoListChangedonTodoListChanged

模型
bindTodoListChanged(callback) {
  this.onTodoListChanged = callback
}

您将在控制器中绑定它,就像视图一样。

控制器
this.model.bindTodoListChanged(this.onTodoListChanged)

现在,在模型中的每个方法之后,您将调用onTodoListChanged回调。

模型
deleteTodo(id) {
  this.todos = this.todos.filter(todo => todo.id !== id)

  this.onTodoListChanged(this.todos)
}
添加本地存储

至此,应用程序基本完成,所有概念都已演示。我们可以通过将数据持久化在浏览器的本地存储中来让它更持久一点,这样刷新后它会在本地持久化。

如果您不了解本地存储的工作原理,请阅读如何通过 JavaScript 使用本地存储。

现在我们可以将初始 todo 值设置为本地存储或空数组中的内容。

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

我们将创建一个commit私有方法来更新localStorage模型状态的值。

模型
_commit(todos) {
  this.onTodoListChanged(todos)
  localStorage.setItem('todos', JSON.stringify(todos))
}

每次更改为 后this.todos,我们都可以调用它。

模型
deleteTodo(id) {
  this.todos = this.todos.filter(todo => todo.id !== id)

  this._commit(this.todos)
}
添加实时编辑功能

这个难题的最后一部分是编辑现有待办事项的能力。编辑总是比添加或删除要复杂一些。我想让它变得简单,不需要编辑按钮或用spananinput或任何东西替换。我们也不想在editTodo每次输入字母时调用 ,因为它会重新渲染整个待办事项列表 UI。

我决定在视图上创建一个方法,用新的编辑值更新临时状态变量,另一个调用handleEditTodo控制器中更新模型的方法。input事件是在您输入contenteditable元素时触发的事件,并在focusout您离开contenteditable元素时触发。

视图
constructor() {
  // ...
  this._temporaryTodoText
  this._initLocalListeners()
}

// Update temporary state
_initLocalListeners() {
  this.todoList.addEventListener('input', event => {
    if (event.target.className === 'editable') {
      this._temporaryTodoText = event.target.innerText
    }
  })
}

// Send the completed value to the model
bindEditTodo(handler) {
  this.todoList.addEventListener('focusout', event => {
    if (this._temporaryTodoText) {
      const id = parseInt(event.target.parentElement.id)

      handler(id, this._temporaryTodoText)
      this._temporaryTodoText = ''
    }
  })
}

现在,当您单击任何待办事项时,您将进入“编辑”模式,该模式将更新临时状态变量,当您在待办事项之外进行 Tab 或单击时,它将保存在模型中并重置临时状态。

只要确保绑定editTodo处理程序。

控制器
this.view.bindEditTodo(this.handleEditTodo)

contenteditable解决方案很快得到实施。contenteditable在生产应用程序中使用时,您需要考虑各种问题,其中很多我已经在这里写过。

结论

你有它。一个纯 JavaScript 中的无依赖 todo 应用程序,演示了模型-视图-控制器架构的概念。这是完整演示和源代码的链接。

查看演示查看源代码

我希望本教程能帮助你理解 MVC。使用这种松散耦合模式可以为应用程序添加大量样板和抽象,但它也是一种可预测的、熟悉的模式,在许多框架中普遍使用,并且作为开发人员需要了解的一个重要概念。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/1296146.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-06-10
下一篇2022-06-10

发表评论

登录后才能评论

评论列表(0条)

    保存