Webprogramozás

Alkalmazásfejlesztési alapelvek, kódszervezés

Horváth Győző
egyetemi docens
horvath.gyozo@inf.elte.hu

1117 Budapest, Pázmány Péter sétány 1/c., 4.725

Ismétlés

Ismétlés - nyelvi elemek

  • Dinamikusan típusos
  • Interpretált nyelv
  • Szintaxis: { utasítások }
  • Adatszerkezetek (elemi, összetett)
    • [1, 2, 3] – tömb
    • { nev: "Győző" } – objektum
  • Funkcionális aspektus
  • OOP-s aspektus

Ismétlés - DOM

  • HTML elemek belső ábrázolása
  • Programozási interfész (API)
  • Bemeneti-kimeneti interfész

Ismétlés – DOM

  • Elemek kiválasztása
    • document.querySelector('css selector')
    • document.querySelectorAll('css selector')
  • Elem (JavaScript objektum) tulajdonságai
    • Analógia: Webfejlesztés → Webprogramozás
    • írás/olvasás
    • tulajdonságok (pl. innerHTML)
    • metódusok
  • Eseménykezelés
    • Eseménytípusok

Ismétlés – Eseménykezelés

  • elem.addEventListener(type, handler)
  • Interakció eszköze
  • Mini-programok
  • Eseményobjektum (event)
  • Alapértelmezett műveletek megakadályozása
    (preventDefault)
  • Buborékolás
  • Delegálás (delegate)

Alkalmazásfejlesztés lépései

Feladatmegoldás lépései

  • Felhasználói felület (UI)
    • Tervezés
    • HTML, CSS
  • Logika, működés, viselkedés
    • Üzleti logika (BL, konzolos alkalmazás)
      • adatok
      • függvények (lekérdezés, módosítás)
    • Eseménykezelő függvények
      • beolvasás (DOM)
      • feldolgozás (BL)
      • kiírás (DOM)
        • imperatív
        • deklaratív

Fizikai és logikai egységek

Kódszervezés az üzleti logikai rétegben

Kódszervezés

  • Fizikai
    • külön fájl
    • modul
  • Logikai
    • függvény
    • osztály (egységbe zárás)
    • modul

Fizikai csoportosítás

  • Külön fájlokba funkció szerint
  • Függőségek kézi kezelése
// math.js
const add = (a, b) => a + b;
// app.js
console.log(add(40, 2));
<body>
  <!-- ... -->
  ✒><script src="math.js"></script><✒
  <script src="app.js"></script>
</body>

Fizikai csoportosítás

  • Külön fájlokba: modul
  • Függőségek automatikus kezelése
// math.js
const add = (a, b) => a + b;
export { add };
// app.js
import { add } from "./math.js";
console.log(add(40, 2));
<body>
  <!-- ... -->
  <script ✒>type="module"<✒ src="app.js"></script>
</body>

Modulok – export

// in-place export
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// separate export
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
export { add, multiply };

// default export
export default const add = (a, b) => a + b;

// rename exports
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
export { add as customAdd, multiply as customMultiply };

// export from module
export * from "./other.js";

Referencia

Modulok – import

// import entities
import { add, multiply } from "./math.js";

// import defaults
import add from "./math.js";

// rename imports
import { add as mathAdd } from "./math.js";

// import module object
import * as MyMath from "./math.js";

// import just for side effects
import "./something.js";

// import URL
import * as MyMath from "http://some.where.hu/math.js"

Referencia

Modulok

  • Strict mode
  • Nem globális scope
    • nehezebb debugolás
      (konzolon nem érhető el)
  • File protocol-on nem működik
  • Node.js serve package
    • Node.js és npm installálása
    • npx serve
    • http://localhost:5000
  • VS Code Live Server extension

Logikai csoportosítás

  • Függvények
    • elemi egység
    • műveletek strukturálása
// Helper/utility function
function range(n) {
  return Array.from({length: n}, (e, i) => i + 1);
}

// HTML generator
function genList(list) {
  return `<ul>${list.map(e => `<li>${e}</li>`).join('')}</ul>`;
}

// Business logic
const add = (a, b) => a + b;

// Event handler
function onClick(e) {
  // ...
}

Logikai csoportosítás

  • Osztályok
    • magasabb szintű egység
    • műveletek és adatok egységbe zárása
class Tile {
  constructor(x, y) {
    this.x = y;
    this.y = y;
  }

  get coords() {
    return {x, y};
  }

  distance(tile) {
    return Math.sqrt(
      (tile.x - this.x) ** 2 + (tile.y - this.y) ** 2
    );
  }
}

Egységbe zárás

Egységbezárás 1.

Globális változók és metódusok

let number = 0;

function increase() {
  number++;
}

function init() {
  number = 0;
}

increase();
console.log(number); // 1

Egységbezárás 2.

Objektum, ~névtér

const game = {
  number: 0,
  increase: function() {
    this.number++;
  },
  init: function () {
    this.number = 0;
  }
}

game.init();
game.increase();
console.log(game.number); // 1

Egységbezárás 3.

Osztály

class Game {
  constructor() {
    this.number = 0;
  }
  increase() {
    this.number++;
  }
}

const game = new Game();
game.increase();
console.log(game.number); // 1

Egységbezárás 4.

Függvény, saját hatókörrel (felfedő modul minta), IIFE

const game = (function () {
  let number = 0;

  function increase() {
    number++;
  }

  function init() {
    number = 0;
  }

  function getNumber() {
    return number;
  }

  return { init, increase, getNumber};
})();

game.init();
game.increase();
console.log(game.getNumber()); // 1

Egységbezárás 5.

Modul, függvény

// game.js
let number = 0;

export function increase() {
  number++;
}

export function init() {
  number = 0;
}

export function getNumber() {
  return number;
}
// main.js
import { increase, init, getNumber } from "./game.js";

increase();
console.log(getNumber()); // 1

Egységbezárás 6.

Modul, osztály

// game.js
export class Game {
  constructor() {
    this.number = 0;
  }
  
  increase() {
    this.number += 1;
  }
}
import { Game } from './game.js';

const game = new Game();
game.increase();
console.log(game.number);

Alkalmazásfejlesztési alapelvek

Eddig

  • JavaScript nyelvi elemek
    • változók, konstansok
    • függvények
    • tömbök, objektumok, osztályok
  • DOM
  • Eseménykezelés
    • eseményobjektum
    • delegálás
  • SZERVEZŐ ELV?

Tanulságok

  • Eseménykezelő függvény
    • beolvasás
    • feldolgozás
    • kiírás
  • Biztosan dolgozik a DOM-mal
  • Kapcsolatot teremt a DOM és a nyelvi elemek között
  • Hol legyen az adat?

Adattárolás helye

Lehetőségek

// ✔ Ebből könnyű HTML-t generálni
let count = 3
const movies = [
  { id: 1, title: 'The Shack',  year: 2017, seen: true  },
  { id: 2, title: 'Fireproof',  year: 2008, seen: true  },
  { id: 3, title: 'Courageous', year: 2011, seen: false },
]
<!-- ✔ Előfordulnak olyan adatok, amelyeket a HTML attribútumban 
        kell tárolni -->
<div data-id="1">The Shack</div>
<!-- ✖ Ebből nehéz az adatot kinyerni -->
<ul>
  <li data-id="1" class="seen"  >The Shack (2017)</li>
  <li data-id="2" class="seen"  >Fireproof (2008)</li>
  <li data-id="3" class="unseen">Courageous (2011)</li>
</ul>

Todo lista


Tárolás a DOM-ban (1)

<input id="newItem">
<button id="addItem">Add</button>
<ul id="todoList">✒>
  <li>Buy milk</li>
  <li>Learn JavaScript</li>
<✒</ul>
const todoList = document.querySelector("#todoList");
const button = document.querySelector("#addItem");
const input = document.querySelector("#newItem");

function handleButtonClick() {
  const newItem = input.value;
  ✒>todoList.innerHTML += `<li>${newItem}</li>`;<✒
}

button.addEventListener("click", handleButtonClick);

Tárolás a DOM-ban (2)

<input id="newItem">
<button id="addItem">Add</button>
<ul id="todoList">
  <li>Buy milk</li>
  <li>Learn JavaScript</li>
</ul>
const todoList = document.querySelector("#todoList");
const button = document.querySelector("#addItem");
const input = document.querySelector("#newItem");

function handleButtonClick() {
  const newItem = input.value;
  ✒>const newListItem = document.createElement("li");<✒
  newListItem.innerHTML = newItem;
  ✒>todoList.appendChild(newListItem);<✒
}

button.addEventListener("click", handleButtonClick);

Tárolás a DOM-ban (3)

Todo lista elemei

<ul id="todoList">✒>
  <li>Buy milk</li>
  <li>Learn JavaScript</li>
<✒</ul>
const todoList = document.querySelector("#todoList");
const listItems = document.querySelectorAll("li");

function handleButtonClick() {
  // Input
  const newItem = input.value;
  ✒>const listContent = Array.from(listItems).map(li => li.innerText);<✒
  // Process
  listContent.push(newItem);
  // Output
  const newListItem = document.createElement("li");
  newListItem.innerHTML = newItem;
  todoList.appendChild(newListItem);
}

Tárolás a DOM-ban

Tárolás a DOM-ban (összefoglalás)

  • Az adatot a DOM-ban tároljuk
  • Mindig onnan kell kiolvasni
  • Nem alap nyelvi elemekkel dolgozunk
  • Adat és feldolgozó függvény szétválik
  • Probléma: egységbe zárás, tárolás

Adat és megjelenés szétválasztása

  • Alkalmazásfejlesztési alapelv
  • Minél kisebb érintkezési felület az adat és felület között
  • Könnyebb egységbe zárás
  • Alapvető nyelvi elemeket használ
  • A DOM (I/O) lassú

Adat és megjelenés szétválasztása

Adat és megjelenés szétválasztása

✒>const list = [];<✒

const input = document.querySelector("input");
const button = document.querySelector("button");
const todoList = document.querySelector("ul");

function handleButtonClick() {
  // Input
  const newItem = input.value;
  // Process
  ✒>list.push(newItem);<✒
  // do something else with the data
  // Output
  const newElement = document.createElement("li");
  newElement.innerHTML = newItem;
  ✒>todoList.appendChild(newElement);<✒
}

button.addEventListener("click", handleButtonClick);

(Imperatív felületkezelés)

Adat és megjelenés szétválasztása

A felület mint az adat leképezése (függvény)

const list = [];

✒>function renderList(list) {
  return list.map(item => `<li>${item}</li>`).join("");
}<✒

// ...

function handleButtonClick() {
  // Input
  const newItem = input.value;
  // Process
  list.push(newItem);
  // Output
  ✒>todoList.innerHTML = renderList(list);<✒
}

button.addEventListener("click", handleButtonClick);

(Deklaratív felületkezelés)

Megoldás részei

Adat és felület szinkronban tartása

Kiírás a DOM-ba

  • Imperatív megközelítés
    • Ha szükséges megőrizni az adott DOM elemet
      (van belső állapotuk, pl. input, animáció, DOM-ban tárolás)
    • Direkt manipuláció
  • Deklaratív megközelítés
    • Ha az elemek újragenerálhatóak (nincs belső állapotuk)
    • Adat leképezése felületi elemekre
    • UI = render(data)
    • HTML generálók

Imperatív

function handleButtonClick() {
  const newItem = input.value;
  list.push(newItem);
  // Output
  ✒>const newElement = document.createElement("li");
  newElement.innerHTML = newItem;
  todoList.appendChild(newElement);<✒
}

Deklaratív

✒>function renderList(list) {
  return list.map(item => `<li>${item}</li>`).join("");
}<✒

const input = document.querySelector("input");
const button = document.querySelector("button");
const todoList = document.querySelector("ul");

function handleButtonClick() {
  // Input
  const newItem = input.value;
  // Process
  list.push(newItem);
  // Output
  ✒>todoList.innerHTML = renderList(list);<✒
}

Példa

Számkitalálós játék

Számkitalálós játék

Felületi terv

<input id="tipp">
<button id="tippGomb">Tipp!</button>
<ul id="tippek"></ul>

Állapottér

let kitalalandoSzam = 42;
let vege = false;
const tippek = [50, 25, 38, 44];

Állapot változása

function tipp(tippeltSzam) {
  tippek.push(tippeltSzam);
  vege = (tippeltSzam === kitalalandoSzam);
}

Segédfüggvények

function veletlenEgesz(min, max) {
  const veletlen = Math.random();
  const tartomany = max - min + 1;
  return Math.trunc(veletlen * tartomany) + min;
}

Eseménykezelő függvények

const tippInput = document.querySelector("#tipp");
const gomb = document.querySelector("#tippGomb");
const tippLista = document.querySelector("#tippek");

gomb.addEventListener("click", tippeles);
function tippeles(e) {
  // beolvasás
  const tippeltSzam = parseInt(tippInput.value);
  // feldolgozás
  tipp(tippeltSzam);
  // kiírás
  // deklaratív felületkezelés
  tippLista.innerHTML = tippek.map(szam => 
    `<li>${szam} (${hasonlit(szam, kitalalandoSzam)})</li>`
  ).join("");
  // imperatív felületkezelés
  gomb.disabled = vege;
}
function hasonlit(szam, kitalalandoSzam) {
  if (szam < kitalalandoSzam) return "nagyobb";
  if (szam > kitalalandoSzam) return "kisebb";
  return "egyenlő";
}

HTML generátorok

function genLista(tippek, kitalalandoSzam) {
  return tippek.map(szam => 
    `<li>${szam} (${hasonlit(szam, kitalalandoSzam)})</li>`
  ).join("");
}
function tippeles(e) {
  // beolvasás
  const tippeltSzam = parseInt(tippInput.value);
  // feldolgozás
  tipp(tippeltSzam);
  // kiírás
  tippLista.innerHTML = genLista(tippek, kitalalandoSzam);
  gomb.disabled = vege;
}

Példa

Memóriajáték

Memóriajáték

Felület, HTML

<div id="main">
  <form action="">
    n = <input type="number" id="n" value="3"> <br>
    m = <input type="number" id="m" value="4">
    <button type="button">Start new game</button>
  </form>
  <div id="board"></div>
  <div id="status"></div>
</div>
<script src="game.js"></script>
<script src="index.js"></script>
<!-- Inside #board -->
<table>
  <tr>
    <td>
      <div class="card">
        <div class="front">1</div>
        <div class="back"></div>
      </div>
    </td>
  </tr>
</table>

Állapottér

let board = []
let gameState = 0 // 0, 1, 2

function init() {
  board = []
  gameState = 0
}
function initBoard(n, m) {
  const numbers = Array(n * m / 2).fill(0).map((e, i) => i + 1)
  const values = [...numbers, ...numbers].sort((a, b) => Math.random() < 0.5 ? 1 : -1)
  board = Array(n).fill(0).map(() => Array(m).fill(0).map(() => ({
    value: values.shift(),
    flipped: false,
    solved: false,
  })))
  gameState = 1
}
function selectCard(i, j) {
  const flipped = flippedCards()
  if (isFlipped(i, j) || isSolved(i, j) || flipped.length === 2) {
    return
  }
  // flip over
  board[i][j].flipped = true
  flipped.push(board[i][j])
  // check cards
  if (flipped.length === 2 && flipped[0].value === flipped[1].value) {
    flipped.forEach(card => {
      card.solved = true
      card.flipped = false
    })
  }
  // check win
  if (isWin()) {
    gameState = 2
  }
}
function isFlipped(i, j) {
  return board[i][j].flipped
}
function isSolved(i, j) {
  return board[i][j].solved
}
function turnBack() {
  board.forEach(row => row.forEach(card => card.flipped = false))
}
function isWin() {
  return board.every(row => row.every(card => card.solved))
}
function flippedCards() {
  return board.flatMap(row => row.filter(card => card.flipped))
}
function countSolved() {
  return board.flatMap(row => row.filter(card => card.solved)).length
}

Eseménykezelők

const form = document.querySelector('form')
const button = form.querySelector('button')
const boardDiv = document.querySelector('#board')
const statusDiv = document.querySelector('#status')

function xyCoord(card) {
  const td = card.closest('td')
  const x = td.cellIndex
  const tr = td.parentNode
  const y = tr.sectionRowIndex
  return { x, y }
}

button.addEventListener('click', onGenerate)
function onGenerate(e) {
  e.preventDefault()
  const n = form.querySelector('#n').valueAsNumber
  const m = form.querySelector('#m').valueAsNumber
  if (n * m % 2 !== 0) {
    return
  }
  initBoard(n, m)
  render()
}

boardDiv.addEventListener('click', onSelectCard)
function onSelectCard(e) {
  const card = e.target.closest('.card')
  if (boardDiv.contains(card)) {
    if (flippedCards().length === 2) {
      return
    }
    const {x, y} = xyCoord(card)
    selectCard(y, x)
    ✒>render()<✒
    if (flippedCards().length === 2) {
      setTimeout(turnBackAndRender, 1000)
    }
  }
}
function turnBackAndRender() {
  turnBack()
  ✒>render()<✒
}

function render() {
  renderBoard(board)
  renderStatus(countSolved(), gameState)
}

function renderBoard(board) {
  boardDiv.innerHTML = `
    <table>
      ${board.map(row => `
        <tr>
          ${row.map(card => `
            <td>
              <div class="card ${card.flipped || card.solved ? 'flipped' : ''} ${card.solved ? 'solved' : ''}">
                <div class="front">${card.value}</div>
                <div class="back"></div>
              </div>
            </td>
          `).join('')}
        </tr>
      `).join('')}
    </table>`
}

function renderStatus(solved, gameState) {
  statusDiv.innerHTML = `
    <p>Game state: ${gameState}</p>
    <p>You have already solved ${solved} cards.</p>
  `
}

Példa – deklaratív

Imperatív kiegészítések

function onSelectCard(e) {
  const card = e.target.closest('.card')
  if (boardDiv.contains(card)) {
    if (flippedCards().length === 2) {
      return
    }
    const {x, y} = xyCoord(card)
    selectCard(y, x)
    ✒>update()<✒
    if (flippedCards().length === 2) {
      setTimeout(turnBackAndRender, 1000)
    }
  }
}
function turnBackAndRender() {
  turnBack()
  ✒>update()<✒
}
function render() {
  renderBoard(board)
  renderStatus(countSolved(), gameState)
}
function update() {
  ✒>updateBoard()<✒
  renderStatus(countSolved(), gameState)
}
✒>function updateBoard() {
  board.forEach((row, i) => row.forEach((card, j) => {
    document.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`).classList.toggle('flipped', card.flipped || card.solved)
    document.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`).classList.toggle('solved', card.solved)
  }))
}<✒

Példa – imperatív

Modularizálás

<!-- ... -->
<script type="module" src="index.js"></script>
import { xyCoord } from "./helper.js";
import { Game } from "./game.js";
import { render, update } from "./render.js";

const form = document.querySelector('form')
const button = form.querySelector('button')
const boardDiv = document.querySelector('#board')
const statusDiv = document.querySelector('#status')

const game = new Game()

button.addEventListener('click', onGenerate)
function onGenerate(e) {
  // ...
  game.initBoard(n, m)
  render(game)
}

boardDiv.addEventListener('click', onSelectCard)
function onSelectCard(e) {
  const card = e.target.closest('.card')
  if (boardDiv.contains(card)) {
    // ...
    game.selectCard(y, x)
    update(game)
    if (game.flippedCards().length === 2) {
      setTimeout(turnBackAndRender, 1000)
    }
  }
}
function turnBackAndRender() {
  game.turnBack()
  update(game)
}

game.js

export class Game {
  constructor() {
    this.board = []
    this.gameState = 0 // 0, 1, 2
  }

  init() {
    this.board = []
    this.gameState = 0
  }
  initBoard(n, m) {
    const numbers = Array(n * m / 2).fill(0).map((e, i) => i + 1)
    const values = [...numbers, ...numbers].sort((a, b) => Math.random() < 0.5 ? 1 : -1)
    this.board = Array(n).fill(0).map(() => Array(m).fill(0).map(() => ({
      value: values.shift(),
      flipped: false,
      solved: false,
    })))
    this.gameState = 1
  }
  selectCard(i, j) {
    const flipped = this.flippedCards()
    if (this.isFlipped(i, j) || this.isSolved(i, j) || flipped.length === 2) {
      return
    }
    // flip over
    this.board[i][j].flipped = true
    flipped.push(this.board[i][j])
    // check cards
    if (flipped.length === 2 && flipped[0].value === flipped[1].value) {
      flipped.forEach(card => {
        card.solved = true
        card.flipped = false
      })
    }
    // check win
    if (this.isWin()) {
      this.gameState = 2
    }
  }
  isFlipped(i, j) {
    return this.board[i][j].flipped
  }
  isSolved(i, j) {
    return this.board[i][j].solved
  }
  turnBack() {
    this.board.forEach(row => row.forEach(card => card.flipped = false))
  }
  isWin() {
    return this.board.every(row => row.every(card => card.solved))
  }
  flippedCards() {
    return this.board.flatMap(row => row.filter(card => card.flipped))
  }
  countSolved() {
    return this.board.flatMap(row => row.filter(card => card.solved)).length
  }
}

render.js

const boardDiv = document.querySelector('#board')
const statusDiv = document.querySelector('#status')

export function render(game) {
  renderBoard(game.board)
  renderStatus(game.countSolved(), game.gameState)
}
export function update(game) {
  updateBoard(game.board)
  renderStatus(game.countSolved(), game.gameState)
}

export function updateBoard(board) {
  board.forEach((row, i) => row.forEach((card, j) => {
    document.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`).classList.toggle('flipped', card.flipped || card.solved)
    document.querySelector(`table tr:nth-child(${i+1}) td:nth-child(${j+1}) .card`).classList.toggle('solved', card.solved)
  }))
}
export function renderBoard(board) {
  boardDiv.innerHTML = `
    <table>
      ${board.map(row => `
        <tr>
          ${row.map(card => `
            <td>
              <div class="card ${card.flipped || card.solved ? 'flipped' : ''} ${card.solved ? 'solved' : ''}">
                <div class="front">${card.value}</div>
                <div class="back"></div>
              </div>
            </td>
          `).join('')}
        </tr>
      `).join('')}
    </table>`
}

export function renderStatus(solved, gameState) {
  statusDiv.innerHTML = `
    <p>Game state: ${gameState}</p>
    <p>You have already solved ${solved} cards.</p>
  `
}

helper.js

export function xyCoord(card) {
  const td = card.closest('td')
  const x = td.cellIndex
  const tr = td.parentNode
  const y = tr.sectionRowIndex
  return { x, y }
}

Összefoglalás

  • Alkalmazásfejlesztési alapelvek
    • adat és megjelenítés szétválasztása
    • adatréteg, eseménykezelők, HTML generátorok
    • felület deklaratív és imperatív kezelése
  • Kódszervezés
    • fizikai, logikai
    • egységbe zárás
    • adattárolás helye