KMI/WEBA Webové aplikace: Seminář 12

Další témata

Doposud představené je postačující pro tvorbu různorodých (architektura, požadavky) i velmi komplexní aplikací. Pro zajímavost si stručně představíme ještě několik technologií, které umožňují využít to co jsme se v tomto kurzu naučili pro tvorbu desktopových a mobilních aplikací.

Nativní aplikace

Položme si následující otázku. Je pro uživatele přívětivější používat webovou aplikaci (nebo stránku) skrze webový prohlížeč nebo použít nativní aplikaci která zprostředkovává stejnou funkcionalitu. Mnohé studie prokazují, že v případě mobilních zařízení je nativní aplikace pro uživatele výrazně přívětivější cesta (výrazně lepší UX). Nativní aplikace je nainstalovaná, má na ploše ikonu, funguje v režimu offline a respektuje UI zásady daného operačního systému.

Z výše uvedených důvodů je tedy velmi zajímavé kromě webové aplikace vyvinout i její mobilní verzi. Samozřejmě vyvinout nativní aplikaci k existující webové aplikaci znamená aplikaci vytvořit (od zátku) pomocí jiných technologií. To může být velmi obtížné, ale jen touto cestou je v současnosti možné možné dosáhnout na z pohledu UX nejlepší řešení. Zjevnou nevýhodou je platformová závislost. Například aplikace pro Android by se měla chovat jinak než aplikace pro iOS (pokud má respektovat zvyklosti daného systému).

Multiplatformní aplikace

Jako alternativa k nativnímu vývoji se nabízí vyvinout multiplatformní (někdy také hybridní) aplikaci. Ty jsou založeny na poměrně jednoduchém principu. Aplikace je tvořena nativní skořápkou (pouze minimální nativní aplikace) obsahující komponentu webview, která zpřístupňuje jádro webového prohlížeče. Skutečná aplikace je webová aplikace, která běží právě v této komponentě. Tento vývoj je poměrně populární jelikož umožňuje vyvinout snadno multiplatformní aplikaci, přičemž je sdílen kód na různých platformách. Nevýhodou je horší UI a UX (byť je v těchto oblastech v poslední době značný pokrok).

Aby se mohla webová aplikace běžící uvnitř webview komponenty chovat jako nativní aplikace, musí mít možnost ovládat hardware daného zařízení. Například zpřístupnit fotoaparát čí datové úložiště. K tomu účelu se využívají frameworky pro vývoj multiplatformních aplikací jako například Apache Cordova nebo PhoneGap. Tento přístup je postupně je postupně utlačován technologiemi typu React native. Ty poskytují API pro přístup k systémovým službám a usnadňují distribuci nativní skořápky (například ExpoGo).

Progresivní webové aplikace

Díky neustále se rozrůstajícím možnostem webových prohlížečů a webových technologií je možné na místo výše uvedených frameworků použít jako interface mezi hardware právě webový prohlížeč. Díky tomu je možné z běžné webové stránky/aplikace vytvořit mobilní nebo desktopovou aplikaci. Aplikace tohoto typu se označují jako progresivní webové aplikace (PWA).

PWA aplikace jsou nezávislé na OS, responzivní, umožňují práci offline, instalovatelné, bezpečné (lze je provozovat pouze na HTTPS) a dostupné skrze URL (není je možné distribuovat skrze aplikační obchody).

Každá progresivní webová aplikace obsahuje manifest soubor, který udržuje základní informace o aplikaci. Offline modu je zajištěno pomocí JavaScriptu, přesněji řečeno servis workera, který slouží jako proxy middleware. Ukázka aktivace service workera následuje.

const registerServiceWorker = async () => {
  //pokud lze ServiceWorkera zaregistrovat
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {scope: "/"});
        
      // odchytávání stavu
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }};
  
  registerServiceWorker();

Service worker zajišťuje uložení celé, nebo alespoň části, aplikace do keše prohlížeče. Při výpadku připojení k síti aplikaci poskytuje data z keše místo toho, aby je stahoval z webu. Tímto je aplikacím umožněno fungovat i offline. Následuje velmi jednoduchý příklad kešování základních dat.

const addResourcesToCache = async (resources) => {
	const cache = await caches.open("v1");
	await cache.addAll(resources);
};
      
self.addEventListener("install", (event) => { 
	// počkáme až je ServiceWorker nainstalovaný
	event.waitUntil(
		addResourcesToCache([ // přidáme soubory do cache
			"/",
			"/index.html",
			"/style.css",
			"/app.js",
			"/image-list.js",
		]),
	);
});

Soubor Manifest.json popisuje metadata aplikace. Mezi ty patří například autor, název aplikace nebo ikonky, které jsou použity při instalaci.


{
	"short_name": "My app",
	"name": "My app is PWA",
	"icons": [
		{
			"src": "icons/icon-512.png",
			"type": "image/png",
			"sizes": "512x512"
		},
		{
			"src": "icons/maskable-icon-192.png",
			"type": "image/png",
			"sizes": "196x196"
		}
	],
	"id":".",
	"start_url": ".",
	"display": "standalone",
	"theme_color": "#000000",
	"background_color": "#ffffff",
	"orientation": "portrait-primary",
	"description":"Simple PWA"
}

Ukázka PWA aplikace je k dispozici na https://github.com/martin-trnecka/pwa. Vyzkoušet je možné aplikaci na adrese https://martin-trnecka.github.io/pwa/.

HTML API

Zpřístupnění hardware je možné skrze HTML API. Například IndexedDB API umožňuje v prohlížeči provozovat relační databázi a ukládat perzistentně data. Analogicky je možné využit Storage API, které jsme již představili. Fotoaparát je možné zpřístupnit pomocí MediaDevices rozhraní.

Electron

Elektron je technologie umožňující vytvářet multiplatformní desktopové aplikace. Princip je analogický jako v případě multiplatformních mobilních aplikací. Elektron poskytuje API, které zpřístupňuje částí operačního systému webové aplikaci, které běží v node.js. Renderování UI je zajištěno pomocí webview komponenty.

Nejprve si vytvoříme kostru aplikace. Se základní konfigurací nám pomůže npm init.

mkdir my-electron-app 
cd my-electron-app
npm init
npm install electron # pouze pokud není naistalován

npm init vytvoří soubor package.json. Ten si mírně upravíme.

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "electron ."
  },
  "author": "Me",
  "devDependencies": {
    "electron": "^21.3.1"
  }
}

Podoba se může lišit dle požadavků. Hlavní je scripts start, který nám spustí aplikaci.

Vytvoříme si html stránku, které bude realizovat okno aplikace.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
    <title>Hello from Electron!</title>
  </head>
  <body>
    <h1>Hello from Electron!</h1>
    <p>👋</p>
  </body>
</html>

Následně vytvoříme skript, který vyrenderuje okno aplikace.

const { app, BrowserWindow } = require('electron');

// vytvoření okna
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
  });
  
  win.loadFile('index.html'); // načtení obsahu okna
};
  
// ovládání a a manipulace s oknem
app.whenReady().then(() => {
  createWindow();
  
  // aplikace na MacOS mohou běžet i bez okna, v takovémto případě
  // znovuspuštění aplikace pouze renderuje nové okno
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});
  
// zavření okna na Windows a Linux, na MacOS (darwin) se nepoužívá
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

Aplikaci spustíme příkazem níže.

npm run start

Hlavní instance electron aplikace běží v omezeném node.js, který má přístup k operačnímu systému. Z bezpečnostních důvodů je renderování UI mimo node.js (stará se o něj jádro prohlížeče). K dispozici je celá řada Electron modulů (API), které zabezpečují různé (běžné) funkce a UI prvky, například Notification, Menu, net a mnoho dalších.

Propojení všech částí elektron aplikace realizuj preoload skript. Kód uložený v souboru preload.js následuje.

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('data', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
  team: Array('Jack', 'Samantha', 'Daniel', 'Teal\'c'),
});

Použití preload skriptu uvedeme v metodě createWindow(), kterou jsme ukázali dříve.

const path = require('path')

// vytvoření okna
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // potřebné kvůli správným adresám na různých OS
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  win.loadFile('index.html'); // načtení obsahu okna
};

Přidáme skript pro úpravu renderování do index.html a přidáme potřebné elementy (team a information).

const information = document.getElementById('info');
information.innerText = `This app is using Chrome (v${data.chrome()}), Node.js (v${data.node()}), and Electron (v${data.electron()})`;
  
const team = document.getElementById('team');
data.team.forEach((t) => team.innerHTML += `

${t}

`);

Pokud vše funguje, měl by výsledek vypadat následovně.

Ukázka aplikace

Propojení mezi aplikací a oknem je třeba realizovat pomocí IPC (jedná se o samostatné procesy). Nejprve rozšíříme preload.js.

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('data', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
  team: Array('Jack', 'Samantha', 'Daniel', 'Teal\'c'),
  ping: () => ipcRenderer.invoke('ping'), // IPC
});

Přidáme obsluhu v okně.

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path')

// vytvoření okna
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  ipcMain.handle('ping', () => 'pong'); // obsluha
  win.loadFile('index.html'); // načtení obsahu okna
};

Do souboru index.html přidáme tlačítko a do souboru render.js jeho obsluhu.

// obsluha tlačítka
const button = document.getElementById('button');

button.addEventListener('click', async () => {
  const response = await window.data.ping();
  information.innerHTML = response;
});