Vytvořte jednoduchou aplikaci MVC od nuly v JavaScriptu

Chtěl jsem napsat jednoduchou aplikaci v prostém JavaScriptu pomocí architektonického vzoru model-view-controller. Tak jsem to udělal a tady to je. Doufejme, že vám to pomůže porozumět MVC, protože je to obtížné pochopit, když začínáte.

Vytvořil jsem tuto aplikaci todo, což je jednoduchá malá aplikace prohlížeče, která vám umožňuje CRUD (vytvářet, číst, aktualizovat a mazat) úkoly. Skládá se pouze z index.html , style.css a script.js , tak pěkné a jednoduché a bez závislosti/rámců pro účely učení.

Předpoklady

  • Základní JavaScript a HTML
  • Znalost nejnovější syntaxe JavaScriptu

Cíle

Vytvořte si todo aplikaci v prohlížeči pomocí prostého JavaScriptu a seznamte se s koncepty MVC (a OOP – objektově orientovaného programování).

  • Zobrazit ukázku
  • Zobrazit zdroj

Co je ovladač zobrazení modelu?

MVC je jedním z možných vzorů pro uspořádání vašeho kódu. Je to populární.

  • Model - Spravuje data aplikace
  • Zobrazit - Vizuální reprezentace modelu
  • Ovladač - Propojuje uživatele a systém

model jsou data. V této aplikaci todo to budou skutečné úkoly a metody, které je přidají, upraví nebo odstraní.

Zobrazení je způsob zobrazení dat. V této todo aplikaci to bude vykreslený HTML v DOM a CSS.

ovladač spojuje model a pohled. Přebírá uživatelský vstup, jako je kliknutí nebo psaní, a zpracovává zpětná volání pro uživatelské interakce.

Model se nikdy nedotýká pohledu. Pohled se nikdy nedotýká modelu. Ovladač je připojí.

Počáteční nastavení

Toto bude plně JavaScriptová aplikace, což znamená, že vše bude řešeno pomocí JavaScriptu a HTML se bude skládat pouze z jediného kořenového prvku v těle.

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>

Napsal jsem malý kousek CSS, aby to vypadalo přijatelně, které najdete zde a uložte do style.css . Víc o CSS psát nebudu, protože to není předmětem tohoto článku.

Dobře, takže teď, když máme HTML a CSS, je čas začít psát aplikaci.

Začínáme

Uděláme to opravdu pěkné a jednoduché, abychom pochopili, jaká třída se týká jaké části MVC. Udělám Model třída, View třída a Controller třídy, která přebírá model a pohled. Aplikace bude instancí ovladače.

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())

Velmi pěkné a abstraktní.

Model

Nejprve se zaměříme na model, protože je ze všech tří částí nejjednodušší. Nezahrnuje žádné události ani manipulaci DOM. Jde pouze o ukládání a úpravu dat.

Model
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,
    )
  }
}

Máme addTodo , editTodo , deleteTodo a toggleTodo . To vše by mělo být velmi samozřejmé – add připojí k poli nový úkol, edit najde id úkolu, který chcete upravit a nahradí jej, vymaže filtry úkolu z pole a přepne přepínače complete booleovská vlastnost.

Protože to vše děláme v prohlížeči a aplikace je přístupná z okna (globálně), můžete je snadno otestovat zadáním něčeho jako:

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

přidá úkol do seznamu a můžete zaznamenat obsah app.model.todos .

To teď modelu stačí. Nakonec úkoly uložíme do místního úložiště, aby to bylo polopermanentní, ale prozatím se úkoly pouze obnoví, kdykoli obnovíte stránku.

Jak vidíme, model se zabývá pouze skutečnými daty a tato data upravuje. Nerozumí ani nezná vstup - co jej mění, nebo výstup - co se nakonec zobrazí.

V tuto chvíli máte vše, co potřebujete pro plně funkční aplikaci CRUD, pokud ručně zadáte všechny své akce prostřednictvím konzole a zobrazíte výstup v konzole.

Zobrazit

Pohled vytvoříme manipulací s DOM - objektový model dokumentu. Protože to děláme v prostém JavaScriptu bez pomoci React's JSX nebo šablonovacího jazyka, bude to trochu upovídané a ošklivé, ale taková je povaha přímé manipulace s DOM.

Řadič ani model by neměli vědět nic o DOM, HTML prvcích, CSS ani o ničem z toho. Vše, co s tím souvisí, by mělo být v zobrazení.

První věc, kterou udělám, je prostě vytvořit pomocné metody pro načtení prvku a vytvoření prvku.

Zobrazit
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
  }
}

Zatím je vše dobré. Nyní v konstruktoru nastavím všechny věci, které potřebuji pro svůj pohled. To bude:

  • Kořenový prvek aplikace – #root
  • Nadpis názvu – h1
  • Tlačítko formuláře, vstupu a odeslání pro přidání úkolu – form , input , button
  • Seznam úkolů – ul

Udělám je všechny proměnné v konstruktoru, abychom na ně mohli snadno odkazovat.

Zobrazit
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)
  }
  // ...
}

Nyní jsou nastaveny části pohledu, které se nezmění.

Další dvě maličkosti - getter a reseter vstupní hodnoty (new todo).

Zobrazit
get _todoText() {
  return this.input.value
}

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

Všechna nastavení jsou nyní hotová. Nejsložitější částí je zobrazení seznamu úkolů, což je část, která se změní pokaždé, když je v úkolech provedena změna.

Zobrazit
displayTodos(todos) {
  // ...
}

displayTodos metoda vytvoří ul a li ze kterých se skládá seznam úkolů a zobrazit je. Pokaždé, když se úkol změní, přidá nebo odstraní, zobrazí se displayTodos metoda bude volána znovu s todos z modelu, resetování seznamu a jejich opětovné zobrazení. To udrží zobrazení v synchronizaci se stavem modelu.

První věc, kterou uděláme, je odstranit všechny todo uzly pokaždé, když je volán. Potom zkontrolujeme, zda existují nějaké úkoly. Pokud ne, zobrazíme zprávu o prázdném seznamu.

Zobrazit
// 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 {
  // ...
}

Nyní projdeme úkoly a zobrazíme zaškrtávací políčko, rozpětí a tlačítko pro smazání pro každý existující úkol.

Zobrazit
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)
  })
}

Nyní je nastaven pohled a je nastaven model. Nemáme způsob, jak je propojit – žádné události nesledují, aby uživatel mohl zadávat vstup, a žádné handlery, které by zpracovávaly výstup takové události.

Konzole stále existuje jako dočasný ovladač a můžete prostřednictvím ní přidávat a odebírat úkoly.

Ovladač

Nakonec je řadič spojovacím článkem mezi modelem (daty) a pohledem (to, co uživatel vidí). Zde je to, co zatím v ovladači máme.

Ovladač
class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view
  }
}

Naším prvním spojením mezi pohledem a modelem je vytvoření metody, která volá displayTodos pokaždé, když se úkol změní. Můžeme to také jednou zavolat v constructor pro zobrazení úvodních úkolů, pokud nějaké existují.

Ovladač
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)
  }
}

Ovladač bude zpracovávat události po jejich spuštění. Když odešlete nový úkol, kliknete na tlačítko Odstranit nebo kliknete na zaškrtávací políčko úkolu, spustí se událost. Pohled musí těmto událostem naslouchat, protože jde o uživatelský vstup pohledu, ale odpovědnost za to, co se stane v reakci na událost, přenese na řadič.

Vytvoříme obslužné rutiny pro události v ovladači.

Ovladač
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)
}

Nastavení posluchačů událostí

Nyní máme tyto handlery, ale kontrolor stále neví, kdy je zavolat. Posluchače událostí musíme umístit na prvky DOM v pohledu. Odpovíme na submit událost ve formuláři a click a change události na seznamu úkolů. (Prozatím přeskakuji "Upravit", protože je to trochu složitější.)

Zobrazit
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)
    }
  })
}

Potřebujeme zavolat handler z pohledu, takže metody, které naslouchají událostem, navážeme na pohled.

Ovladač
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

Nyní, když submit , click nebo change událost se stane na zadaných prvcích, budou vyvolány odpovídající handlery.

Reagovat na zpětná volání v modelu

Něco jsme vynechali – události naslouchají, jsou vyvoláni handleři, ale nic se neděje. Je to proto, že model neví, že by se pohled měl aktualizovat, a neví, co udělat, aby se pohled aktualizoval. Máme displayTodos Metoda pohledu to řeší, ale jak již bylo zmíněno dříve, model a pohled by o sobě neměly vědět.

Stejně jako u naslouchání událostem by se měl model vrátit zpět do ovladače, aby věděl, že se něco stalo.

Již jsme vytvořili onTodoListChanged jak se s tím vypořádat, musíme na to model upozornit. Připojíme jej k modelu stejným způsobem, jako jsme to udělali s ovladači na pohledu.

V modelu přidejte bindTodoListChanged pro onTodoListChanged .

Model
bindTodoListChanged(callback) {
  this.onTodoListChanged = callback
}

A toto svážete v ovladači, stejně jako u pohledu.

Ovladač
this.model.bindTodoListChanged(this.onTodoListChanged)

Nyní po každé metodě v modelu zavoláte onTodoListChanged zpětné volání.

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

  this.onTodoListChanged(this.todos)
}

Přidat místní úložiště

V tomto okamžiku je aplikace z velké části dokončena a všechny koncepty byly předvedeny. Můžeme to udělat trochu trvalejší tím, že uchováme data v místním úložišti prohlížeče, takže po obnovení zůstanou lokálně.

Nyní můžeme nastavit počáteční hodnotu úkolu na to, co je v místním úložišti nebo prázdné pole.

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

Vytvoříme commit private metoda k aktualizaci hodnoty localStorage stejně jako stav modelu.

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

Po každé změně na this.todos , můžeme to nazvat.

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

  this._commit(this.todos)
}

Přidat funkci živých úprav

Posledním kouskem této skládačky je možnost upravit existující úkol. Úpravy jsou vždy o něco složitější než přidávání nebo mazání. Chtěl jsem to zjednodušit a nevyžadovat tlačítko pro úpravy nebo nahrazování span s input nebo cokoliv. Také nechceme volat editTodo pokaždé, když je napsáno písmeno, protože to znovu vykreslí celé uživatelské rozhraní seznamu úkolů.

Rozhodl jsem se vytvořit metodu na pohledu, která aktualizuje dočasnou stavovou proměnnou novou hodnotou pro úpravy a další, která volá handleEditTodo metoda v ovladači, která aktualizuje model. input událost je to, co se spustí, když zadáte contenteditable prvek a focusout spustí se, když opustíte contenteditable prvek.

Zobrazit
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 = ''
    }
  })
}

Nyní, když kliknete na libovolnou položku úkolu, vstoupíte do režimu „úpravy“, který aktualizuje proměnnou dočasného stavu, a když tabulátor nebo klepnutí opustíte úkol, uloží se do modelu a obnoví se dočasný stav.

Jen se ujistěte, že jste svázali editTodo handler.

Ovladač
this.view.bindEditTodo(this.handleEditTodo)

Závěr

Tady to máš. Bezzávislá todo aplikace v prostém JavaScriptu, která demonstruje koncepty architektury model-view-controller. Zde je ještě jednou odkaz na dokončené demo a zdroj.

  • Zobrazit ukázku
  • Zobrazit zdroj

Doufám, že vám tento tutoriál pomohl pochopit MVC. Použití tohoto volně propojeného vzoru může přidat aplikaci hodně standardní a abstrakci, ale je to také předvídatelný, známý vzor, ​​který se běžně používá v mnoha frameworkech, a důležitý koncept, který jako vývojář musíte znát.