Electron, 2. část

HTML, JavaScript, CSS

V minulém díle byl představen Electron framework s jeho silnými i slabými stránkami. Na tento úvod navážeme a podíváme se, jak fungují jeho procesy a komunikace mezi nimi.

Electron, 2. část

Electron, 1. část - Úvod, příklad, výhody a nevýhody 
Electron, 2. část - Hlavní a vykreslovací procesy a jejich vzájemná komunikace

Hlavní a vykreslovací procesy

Electron využívá dva typy procesů - hlavní (main) a vykreslovací (renderer).

Jakmile je Electron spuštěn, vytvoří hlavní proces a spustí v něm skript, který je uveden jako hlavní v souboru package.json. Hlavní proces je srdce celé aplikace a je vždy pouze jeden. Z něj jsou pak vytvářeny například okna uživatelského rozhraní, nativní dialogová okna, menu a podobně. Taky řeší životní cykly aplikace jako zapínání, minimalizace a uzavření.

Vykreslovacích procesů může být libovolné množství a starají se pouze o vykreslení webové stránky, tedy uživatelského rozhraní. Není ale pravda, že jedno okno aplikace rovná se jeden proces. Jedno okno může totiž obsahovat více webových stránek, například v tzv. webview, což je stránka ve stránce podobná iframe v běžném webu. Vlastní proces je tak vytvořený pro každou webovou stránku, ať už načtenou z HTML souboru nebo vzdáleně. Vykreslovací proces může být vytvořen jen z hlavního procesu.

Izolace procesů

A když se bavíme o procesech, mám tím namysli běžné systémové procesy. Pokud si spustím jakoukoliv Electron aplikaci, uvidím ve správci procesů minimálně dva procesy spadající pod Electron - hlavní a vykreslovací.

Proč tomu tak je? Electron používá pro vykreslování webových stránek Chromium. To vytváří pro každou záložku/okno nový proces. Ty jsou pak úplně izolované, což nese určité výhody. Pokud by se třeba stalo, že některá ze stránek zamrzne, neovlivní to ostatní a ani samotný prohlížeč. Je to příznivé i z hlediska bezpečnosti. Potenciální nebezpečný skript nemá přístup k jiným zdrojům, než v dané záložce. Electron převzal podobnou filozofii a každý proces tak má svůj vlastní píseček.

Rozdílná API

Protože oba druhy procesů mají jiné povinnosti, mají každý k dispozici trochu jinou část Electron API. Hlavní proces má dohled nad celou aplikací, může vytvářet například nová okna, dialogová okna a kontextová menu. Vykreslovací proces má pak přístup ke konfiguraci vykreslování stránky (zoom, spellcheck, JS sandbox) nebo webkameře a mikrofonu. V dokumentaci je u každého modulu uvedeno, kterému procesu je k dispozici.

Vykreslovací proces má navíc ještě i přístup k DOM/prohlížečovému API. Oba dva pak mají přístup k celému Node API.

Komunikace

Je jasné, že procesy budou mezi sebou potřebovat komunikovat. Například pro vytvoření nového okna po kliknutí na tlačítko, musí vykreslovací proces dát vědět hlavnímu, že ho chce vytvořit. Ke komunikaci Electron používá IPC - interprocess communication (stejně jako Chromium). V podstatě jde o zprávy mezi vykreslovacím a hlavním procesem, které mají nějaké jméno a náklad nějakých dat.

Pro zajímavost - IPC používá pro komunikaci tzv. named pipes, které jsou rozšířením unixových pipeline. Uživatelům Linux/OS X budou známé z příkazů podobných:

ls | grep "txt"

Prakticky je v API komunikace řešena dvěma způsoby: IPC a remote moduly.

IPC

IPC modul má dvě části: ipcMain a ipcRenderer. Každý z nich je dostupný pouze pro odpovídající typ procesu. Fungují velmi podobně jako běžné eventy (více o eventech v JS) a obsahují metody pro odeslání i příjem zpráv. Modul má jak asynchronní, tak synchronní metody pro odesílání zpráv.

Modul ipcRenderer umožňuje komunikaci z vykreslovacího do hlavního procesu.

const { ipcRenderer } = require('electron')

// Odeslání asynchronní zprávy do hlavního procesu
// První parametr je název
// Další parametry jsou data
ipcRenderer.send('hlavnimuProcesu', 'ahoj')

// Příjem asynchronní zprávy z hlavního procesu
ipcRenderer.on('zHlavnihoProcesu', (event, messages) => {
  console.log(messages) // "no nazdar"
})

// Odesílání synchronní zprávy do hlavního procesu 
// a přijetí odpovědi
const reply = ipcRenderer.sendSync('hlavnimuProcesuSync', 'ping')
console.log(reply) // "pong"

Modul ipcMain obstarává komunikaci opačným směrem.

const { ipcMain } = require('electron')

// Přijímá asynchronní zprávy z vykreslovacího procesu
ipcMain.on('hlavnimuProcesu', (event, messages) => {
  console.log(messages) // "ahoj"
  // Odešle odpověď vykreslovacímu procesu
  event.sender.send('zHlavnihoProcesu', 'no nazdar')
})

// Přijímá synchronní zprávy z vykreslovacího procesu
ipcMain.on('hlavnimuProcesuSync', (event, messages) => {
  console.log(messages) // "ping"
  // Odešle odpověď vykreslovacímu procesu
  event.returnValue = 'pong'
})

Remote

Modul remote vystavuje Electron API dostupné pro hlavní proces. Tím umožní přímo ve vykreslovacím procesu např. vytvářet nová okna.

const { remote } = require('electron')
const { BrowserWindow } = remote

// Vytvoří nové okno přímo z vykreslovacího procesu
let win = new BrowserWindow({
  width: 800,
  height: 600
})

// Načte do něj stránku blogu
win.loadUrl('https://www.kutac.cz/blog')

Je to velmi jednoduchý a intuitivní přístup k API hlavního procesu, ale na pozadí stejně používá synchronní IPC komunikaci. Tím pádem může být vykreslovací proces zastaven a remote se tak moc nehodí na náročnější úlohy. Doporučoval bych ve většině případů použít rovnou IPC.

Načtení souboru ze systému

Jako příklad komunikace mezi procesy se podíváme na načtení souboru ze systému pomocí nativního dialogového okna. 

Máme soubor main.js obsahující logiku hlavního procesu a index.html, obsahující stránku zobrazenou ve vykreslovacím procesu. Cílem je po kliknutí na tlačítko ve stránce otevřít dialogové okno pro výběr souboru v systému, ten pak načíst a jeho obsah vypsat do stránky. Nativní dialogové okno může vytvořit pouze hlavní proces, takže je potřeba mezi procesy komunikovat.

<!-- index.html -->
<!-- ... -->

<textarea readonly id="file-contents"></textarea>
<button id="dialog-opener">Otevřít soubor</button>

<script>
  const { ipcRenderer } = require('electron')

  const button = document.querySelector('#dialog-opener')
  const textarea = document.querySelector('#file-contents')

  // Po kliknutí na tlačítko odešle zprávu hlavnímu procesu
  button.addEventListener('click', event => {
    ipcRenderer.send('openDialog')
  })

  // Po přijetí zprávy z hlavního procesu
  // obsahující data načtená ze souboru je zobrazí ve stránce
  ipcRenderer.on('fileRead', (event, message) => {
    textarea.innerText = message
  })
</script>
// main.js
// ...

const { ipcMain, dialog } = require('electron')
const fs = require('fs')

// Po přijetí zprávy z vykreslovacího procesu otevře
// nativní dialog pro výběr souboru
ipcMain.on('openDialog', (event, message) => {
  const filePaths = dialog.showOpenDialog()
  // Dialog po výběru vrací pole cest k souborům
  // Vybíráme jeden, takže nás zajímá jen ta první
  const filePath = filePaths[0]
  const fileData = fs.readFileSync(filePath, 'utf8')

  event.sender.send('fileRead', fileData)
})

Co se stane je, že po kliknutí na tlačítko se pošle zpráva hlavnímu procesu. Ten ji zachytí a otevře dialogové okno. Dialog vrátí cestu k vybranému souboru, který se načte a následně pošle hlavní proces zprávu s obsahem souboru. Vykreslovací proces zprávu zachytí a její obsah vypíše do stránky.

Závěrem

Výpočetně velmi náročné úlohy nenechávejte na hlavním procesu. Osobně se mi zdálo jako dobrý postup přenechávat náročné úlohy hlavnímu procesu, aby nebylo blokováno uživatelské rozhraní. Ve skutečnosti tím docílíte jen zpomalení všech vykreslovacích procesů a aplikace jako takové. Doporučuji držet tyto úlohy ve vykreslovacím vlákně. Zajímavé řešení ještě může být třeba knihovna electron-remote a její renderer taskpool, který na pozadí vytvoří až 4 vykreslovací procesy pro souběžné zpracování úloh a dokáže je škálovat podle potřeby.

Jak jsem zmínil výše, jednotlivé procesy, ať už hlavní nebo vykreslovací, mají izolovanou pamět i stav. Takže pokud si ve dvou procesech načtu stejný modul a provedu v něm nějaké změny, ty změny budou pro každý proces vlastní. Nepropíší se do ostatních.

Stejně jako v minulém díle, znova doporučuji projít si repozitář awesome-electron pro inspiraci. Třeba torrent klient WebTorrent má poměrně čitelnou architekturu.


Snad teď máte trochu lepší představu o struktuře procesů v Electronu a práce s nimi. Pokud ne, můžete mi vynadat v komentářích :)

Přidat komentář

Položky označené * jsou povinné. Email nebude zveřejněn

Buď první, kdo přidá komentář. Zatím zde nic není