用纯JavaScript写一个简单的MVC App

#头条创作挑战赛#

我想使用model-view-controller体系结构模式并用纯JavaScript编写一个简单的应用程序。所以我着手做了,下面就是。希望能帮你理解MVC,因为这是一个很难理解的概念,刚接触时候会很疑惑。

我制作了this todo app,它是一个简单的浏览器小应用程序,你可以进行CRUD(create, read, update, delete)操作。它仅由index.html,style.less和script.js文件组成,非常棒而且简单,无需安装依赖/无需框架就可以学习。

前置条件

  • 基本的JavaScript和HTML知识
  • 熟悉the latest JavaScript syntax
  • 目标

    用纯JavaScript在浏览器中创建一个待办事项程序(a todo app),并且熟悉MVC概念(和OOP-object-oriented programming,面向对象编程)。

  • View demo
  • View source
  • 因为这个程序使用了最新的JavaScript特性(ES2017),在不使用Babel编译为向后兼容的JavaScript语法的情况下,在Safari这样的浏览器上无法按照预期工作。

    什么是MVC?

    MVC是组织代码的一种模式。它是受欢迎的模式之一。

  • Model – 管理应用程序的数据
  • View – Model的可视化表示(也就是视图)
  • Controller – 连接用户和系统
  • view是数据的显示方式。在此代办事项应用程序中,这将是DOM和CSS呈现出来的HTML。

    controller连接modelview。它接受用户输入,比如单击或者键入,并处理用户交互的回调。

    model永远不会触及viewview永远不会触及modelcontroller将它们连接起来。

    我想说的是,在这个简单的 todo app 中使用 MVC 大才小用。如果这是你要创建的应用程序,并且整个系统都由你自己开发,那确实会使得事情变得过于复杂。重点是尝试从一个较小的角度了解它,以便你可以理解为什么一个可伸缩迭代的系统会使用它。

    初始化设置

    这将是一个完全的JavaScript的应用程序,这就意味着所有的内容将通过JavaScript处理,而HTML在主体中仅包含一个根元素。

    <!--index.html-->br<!DOCTYPE html>br<html lang="en">br  <head>br    <meta charset="utf-8" />br    <meta name="viewport" content="width=device-width, initial-scale=1.0" />br    <meta http-equiv="X-UA-Compatible" content="ie=edge" />brbr    <title>Todo App</title>brbr    <link rel="stylesheet" href="style.css" />br  </head>brbr  <body>br    <div id="root"></div>brbr    <script src="script.js"></script>br  </body>br</html>br复制代码

    好了,现在我们有了HTML和CSS,所以是时候开始写这个应用程序了。

    开始

    我们将使它变得非常好用和简单,以了解哪些类对应MVC的哪部分。

    我将创建一个Model类,一个View类和一个Controller类,它们将包含modelview。该应用是控制器的一个实例。

    如果你不熟悉类是怎么工作的,先去读下Understanding Classes in JavaScript文章

    brclass Model {br  constructor() {}br}brbrclass View {br  constructor() {}br}brbrclass Controller {br  constructor(model, view) {br    this.model = modelbr    this.view = viewbr  }br}brbrconst app = new Controller(new Model(), new View())br复制代码

    非常棒,而且抽象。

    Model

    我们先来处理model先,因为它是三部分中最简单的。它并不涉及任何事件和DOM操作。它只是存储和修改数据。

    br// Modelbrclass Model {br  constructor() {br    // The state of the model, an array of todo objects, prepopulated with some databr    this.todos = [br      { id: 1, text: 'Run a marathon', complete: false },br      { id: 2, text: 'Plant a garden', complete: false },br    ]br  }brbr  addTodo(todoText) {br    const todo = {br      id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1,br      text: todoText,br      complete: false,br    }brbr    this.todos.push(todo)br  }brbr  // Map through all todos, and replace the text of the todo with the specified idbr  editTodo(id, updatedText) {br    this.todos = this.todos.map(todo =>br      todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todobr    )br  }brbr  // Filter a todo out of the array by idbr  deleteTodo(id) {br    this.todos = this.todos.filter(todo => todo.id !== id)br  }brbr  // Flip the complete boolean on the specified todobr  toggleTodo(id) {br    this.todos = this.todos.map(todo =>br      todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todobr    )br  }br}br复制代码

    因为我们都是在浏览器中进行此操作,并且可以从window(golbal)中访问应用程序,因此你可以轻松地进行测试,键入以下内容:

    brapp.model.addTodo('Take a nap')br复制代码

    上面的命令行将添加一件待办事项到列表中,你可以打印出app.model.todos查看。

    这对于当前的model已经足够了。最后,我们将待办事项存储在local storage中,使其成为永久性文件,但目前,待办事项只要刷新页面就可以刷新了。

    如我们所见,model只是处理实际的数据,并修改数据。它不了解或不知道输入 – 正在修改的内容,或输出 – 最终将显示的内容。

    此时,如果你通过控制台手动键入所有操作并在控制台中查看输出,则你的app具备了功能全面的CRUD。

    View

    我们将通过操作DOM(文档对象模型)来创建视图。由于我们在没有React的JSX或模版语言的情况下使用纯JavaScript进行此操作的,因此它有些冗长和丑陋,但是这就是直接操作DOM的本质。

    controllermodel都不应该了解有关DOM、HTML元素、CSS或者其他方面的信息。任何与这些信息相关的东西都应该在view层。

    如果你不熟悉DOM或DOM与HTML源码有何不同,阅读下Introduction to the DOM文章。

    我要做的第一件事情就是创建辅助方法检索一个元素并创建一个元素。

    br// Viewbrclass View {br  constructor() {}brbr  // Create an element with an optional CSS classbr  createElement(tag, className) {br    const element = document.createElement(tag)br    if (className) element.classList.add(className)brbr    return elementbr  }brbr  // Retrieve an element from the DOMbr  getElement(selector) {br    const element = document.querySelector(selector)brbr    return elementbr  }br}br复制代码

    到目前为止一切顺利。在构造器中,我将设置我所需的全部内容。那将会:

  • 应用程序的根元素 – #root
  • 标题 – h1
  • 一个表单,输入框和提交按钮去添加事项 – form,input,button
  • 待办列表 – ul
  • 我将使它们成为构造函数中的所有变量,以便我们可以轻松地引用它们。

    br// Viewbrclass View {br  constructor() {br    // The root elementbr    this.app = this.getElement('#root')brbr    // The title of the appbr    this.title = this.createElement('h1')br    this.title.textContent = 'Todos'brbr    // The form, with a [type="text"] input, and a submit buttonbr    this.form = this.createElement('form')brbr    this.input = this.createElement('input')br    this.input.type = 'text'br    this.input.placeholder = 'Add todo'br    this.input.name = 'todo'brbr    this.submitButton = this.createElement('button')br    this.submitButton.textContent = 'Submit'brbr    // The visual representation of the todo listbr    this.todoList = this.createElement('ul', 'todo-list')brbr    // Append the input and submit button to the formbr    this.form.append(this.input, this.submitButton)brbr    // Append the title, form, and todo list to the appbr    this.app.append(this.title, this.form, this.todoList)br  }br  // ...br}br复制代码

    现在,视图不变的部分已经设置好。

    搜图

    两个小事情 – 输入(新待办事项)值的获取和重置。

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

    br// Viewbrget _todoText() {br  return this.input.valuebr}brbr_resetInput() {br  this.input.value = ''br}br复制代码

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

    br// ViewbrdisplayTodos(todos) {br  // ...br}br复制代码

    displayTodos方法将创建待办事项列表所组成的ul和li,并显示它们。每次更改,添加,或者删除待办事项时,都会使用模型中的待办事项todos,再次调用displayTodos方法,重置列表并显示它们。这将使得视图和模型的状态保持同步。

    我们要做的第一件事是每次调用时都会删除所有待办事项的节点。然后我们将检查是否有待办事项。如果没有,我们将显示一个空列表消息。

    br// Viewbr// Delete all nodesbrwhile (this.todoList.firstChild) {br  this.todoList.removeChild(this.todoList.firstChild)br}brbr// Show default messagebrif (todos.length === 0) {br  const p = this.createElement('p')br  p.textContent = 'Nothing to do! Add a task?'br  this.todoList.append(p)br} else {br  // ...br}br复制代码

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

    br// Viewbrelse {br  // Create todo item nodes for each todo in statebr  todos.forEach(todo => {br    const li = this.createElement('li')br    li.id = todo.idbrbr    // Each todo item will have a checkbox you can togglebr    const checkbox = this.createElement('input')br    checkbox.type = 'checkbox'br    checkbox.checked = todo.completebrbr    // The todo item text will be in a contenteditable spanbr    const span = this.createElement('span')br    span.contentEditable = truebr    span.classList.add('editable')brbr    // If the todo is complete, it will have a strikethroughbr    if (todo.complete) {br      const strike = this.createElement('s')br      strike.textContent = todo.textbr      span.append(strike)br    } else {br      // Otherwise just display the textbr      span.textContent = todo.textbr    }brbr    // The todos will also have a delete buttonbr    const deleteButton = this.createElement('button', 'delete')br    deleteButton.textContent = 'Delete'br    li.append(checkbox, span, deleteButton)brbr    // Append nodes to the todo listbr    this.todoList.append(li)br  })br}br复制代码

    现在视图和模型都设置好了。我们只是还没办法连接它们 – 没有事件监听用户的输入,也没有处理程序来处理此类事件的输出。

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

    搜图

    Controller

    最后,控制器是模型(数据)和视图(用户所见)之间的连接。到目前为止,下面就是控制器中的内容。

    br// Controllerbrclass Controller {br  constructor(model, view) {br    this.model = modelbr    this.view = viewbr  }br}br复制代码

    视图和模型之间的第一个连接是创建一个方法,该方法在每次待办事项更改时调用displayTodos。我们也可以在构造函数中调用一次,以显示初始待办事项,如果有。

    br// Controllerbrclass Controller {br  constructor(model, view) {br    this.model = modelbr    this.view = viewbrbr    // Display initial todosbr    this.onTodoListChanged(this.model.todos)br  }brbr  onTodoListChanged = todos => {br    this.view.displayTodos(todos)br  }br}br复制代码

    触发事件之后,控制器将对其进行处理。当你提交新的待办事项,单击删除按钮或单击待办事项的复选框时,将触发一个事件。视图必须监听那些事件,因为它是视图中用户的输入,但是它将把响应该事件将要发生的事情责任派发到控制器。

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

    br// ViewbrhandleAddTodo = todoText => {br  this.model.addTodo(todoText)br}brbrhandleEditTodo = (id, todoText) => {br  this.model.editTodo(id, todoText)br}brbrhandleDeleteTodo = id => {br  this.model.deleteTodo(id)br}brbrhandleToggleTodo = id => {br  this.model.toggleTodo(id)br}br复制代码

    设置事件监听器

    br// ViewbrbindAddTodo(handler) {br  this.form.addEventListener('submit', event => {br    event.preventDefault()brbr    if (this._todoText) {br      handler(this._todoText)br      this._resetInput()br    }br  })br}brbrbindDeleteTodo(handler) {br  this.todoList.addEventListener('click', event => {br    if (event.target.className === 'delete') {br      const id = parseInt(event.target.parentElement.id)brbr      handler(id)br    }br  })br}brbrbindToggleTodo(handler) {br  this.todoList.addEventListener('change', event => {br    if (event.target.type === 'checkbox') {br      const id = parseInt(event.target.parentElement.id)brbr      handler(id)br    }br  })br}br复制代码

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

    我们使用箭头函数来处理事件。这允许我们直接使用controller的上下文this来调用view中的表单。如果你不使用箭头函数,我们需要手动bind绑定它们,比如this.view.bindAddTodo(this.handleAddTodo.bind(this))。咦~

    br// Controllerbrthis.view.bindAddTodo(this.handleAddTodo)brthis.view.bindDeleteTodo(this.handleDeleteTodo)brthis.view.bindToggleTodo(this.handleToggleTodo)br// this.view.bindEditTodo(this.handleEditTodo) - We'll do this one lastbr复制代码

    现在,当一个submit,click或者change事件在特定的元素中触发,相应的处理事件将被唤起。

    响应模型中的回调

    我们遗漏了一些东西 – 事件正在监听,处理程序被调用,但是什么也没有发生。这是因为模型不知道视图应该更新,也不知道如何进行视图的更新。我们在视图上有displayTodos方法来解决此问题,但是如前所述,模型和视图不互通。

    就像监听起那样,模型应该触发回来控制器这里,以便其知道发生了某些事情。

    我们已经在控制器上创建了onTodoListChanged方法来处理此问题,我们只需要使模型知道它就可以了。我们将其绑定到模型上,就像绑定到视图的方式一样。

    在模型上,为onTodoListChanged添加bindTodoListChanged方法。

    br// ModelbrbindTodoListChanged(callback) {br  this.onTodoListChanged = callbackbr}br复制代码

    然后将其绑定到控制器中,就像与视图一样。

    br// Controllerbrthis.model.bindTodoListChanged(this.onTodoListChanged)br复制代码

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

    br// ModelbrdeleteTodo(id) {br  this.todos = this.todos.filter(todo => todo.id !== id)brbr  this.onTodoListChanged(this.todos)br}br复制代码

    添加 local storage

    至此,该应用程序已基本完成,所有概念都已演示。通过将数据持久保存在浏览器的本地存储中,我们可以使其更加持久,因此刷新后将在本地持久保存。

    如果你不熟悉local storage是怎么工作的,阅读下How to Use Local Storage with JavaScript文章。

    现在,我们可以将初始化待办事项设置为本地存储或空数组中的值。

    br// Modelbrclass Model {br  constructor() {br    this.todos = JSON.parse(localStorage.getItem('todos')) || []br  }br}br复制代码

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

    br_commit(todos) {br  this.onTodoListChanged(todos)br  localStorage.setItem('todos', JSON.stringify(todos))br}br复制代码

    在每次this.todos发生更改之后,我们可以调用它。

    brdeleteTodo(id) {br  this.todos = this.todos.filter(todo => todo.id !== id)brbr  this._commit(this.todos)br}br复制代码
    br// Viewbrconstructor() {br  // ...br  this._temporaryTodoTextbr  this._initLocalListeners()br}brbr// Update temporary statebr_initLocalListeners() {br  this.todoList.addEventListener('input', event => {br    if (event.target.className === 'editable') {br      this._temporaryTodoText = event.target.innerTextbr    }br  })br}brbr// Send the completed value to the modelbrbindEditTodo(handler) {br  this.todoList.addEventListener('focusout', event => {br    if (this._temporaryTodoText) {br      const id = parseInt(event.target.parentElement.id)brbr      handler(id, this._temporaryTodoText)br      this._temporaryTodoText = ''br    }br  })br}br复制代码

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年9月5日
    下一篇 2022年9月5日

    相关推荐