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.
Modelclass 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.
Zobrazitclass 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.
Zobrazitclass 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).
Zobrazitget _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.
ZobrazitdisplayTodos(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.
Zobrazitelse {
// 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í.
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ší.)
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
.
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í.
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.
Modelclass Model {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || []
}
}
Vytvoříme commit
private metoda k aktualizaci hodnoty localStorage
stejně jako stav modelu.
_commit(todos) {
this.onTodoListChanged(todos)
localStorage.setItem('todos', JSON.stringify(todos))
}
Po každé změně na this.todos
, můžeme to nazvat.
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.
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.
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.