Jak jsem vytvořil svou první aplikaci Electron &You Can Too Pt.3 – Uvolnění elektronu

Vytvoření elektronické aplikace nemusí být těžké. Může to být vlastně snadné. V této mini sérii se podíváme, jak na to, krok za krokem. V této části dáme dohromady kód, který spustí naši elektronovou aplikaci. Poté vytvoříme první a také hlavní komponentu React pro naši aplikaci s jednoduchým uživatelským rozhraním. Díky tomu budeme mít konečně šanci spustit naši elektronovou aplikaci a vidět výsledky naší práce. Takže bez dalších okolků začněme!

Jak jsem vytvořil svou první aplikaci Electron &You Can Too, část 1.

Jak jsem vytvořil svou první aplikaci Electron &You Can Too část 2.

Jak jsem vytvořil svou první aplikaci Electron &You Can Too část 4.

Jak jsem vytvořil svou první aplikaci Electron &You Can Too, část 5.

Nastavení Electronu

Pojďme rovnou k vývoji naší elektronové aplikace. Naším prvním krokem bude sestavení souboru s názvem main.js . Jak si možná pamatujete z druhé části, tento soubor by měl být v kořenovém adresáři našeho projektu. Účel tohoto souboru je jednoduchý. Obsahuje skript nazvaný the main process a tento skript je zodpovědný za spuštění hlavního procesu, který poté zobrazí GUI vytvořením webových stránek, což se provede vytvořením jedné nebo více instancí BrowserWindow .

Každá z těchto webových stránek a instancí BrowserWindow také provozuje svůj vlastní renderovací proces. Pokud je jedna webová stránka uzavřena, je uzavřen i její vykreslovací proces. A hlavní proces je něco jako manažer těchto procesů. Je toho mnohem víc a koho to zajímá, podívejte se na Quick Start manual na GitHubu. To však není důležité pro účely sestavování naší elektronové aplikace. Vše, co potřebujeme vědět, je, že tento soubor, main.js , je nezbytný pro provoz naší aplikace.

Naštěstí pro nás s tímto souborem nemusíme tolik dělat. Můžeme použít výchozí verzi souboru poskytovanou modulem electron-quick-start boilerplate. No, skoro. Budeme muset přidat jen několik dalších řádků, abychom se připravili na funkce, které chceme mít v naší elektronové aplikaci, konkrétně možnost minimalizovat naši aplikaci do systémové lišty. Dále přidáme kód pro implementaci kontextového menu. Nakonec budeme také muset provést nějaké změny, abychom mohli implementovat Webpack.

Plná verze main.js soubory, které zapnou naši elektronovou aplikaci, je následující.

'use strict'

// Require electron
const electron = require('electron')

// Module to control application life.
const app = electron.app

// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow

const path = require('path')
const url = require('url')

// Module to check for platform
const platform = require('os').platform()

// Modules to create app tray icon and context menu
const Menu = electron.Menu
const Tray = electron.Tray

// Create variables for icons to prevent disappearing icon when the JavaScript object is garbage collected.
let trayIcon = null
let appIcon = null

// Determine appropriate icon for platform
if (platform == 'darwin') {
  trayIcon = path.join(__dirname, 'src', 'assets/grease-the-groove-icon.png')
} else if (platform == 'win32') {
  trayIcon = path.join(__dirname, 'src', 'assets/grease-the-groove-icon.ico')
}

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow

// Keep a reference for dev mode
let dev = false

if (process.defaultApp || /[\\/]electron-prebuilt[\\/]/.test(process.execPath) || /[\\/]electron[\\/]/.test(process.execPath)) {
  dev = true
}

// Temporary fix broken high-dpi scale factor on Windows (125% scaling)
// info: https://github.com/electron/electron/issues/9691
if (process.platform === 'win32') {
  app.commandLine.appendSwitch('high-dpi-support', 'true')
  app.commandLine.appendSwitch('force-device-scale-factor', '1')
}

function createWindow() {
  // Create the browser window.
  // with specific icon and don't show it until it is ready (show: false)
  mainWindow = new BrowserWindow({
    icon: trayIcon,
    height: 667,
    show: false,
    title: 'Grease the Groove',
    width: 375
  })

  // Create tray icon
  appIcon = new Tray(trayIcon)

  // Create RightClick context menu for tray icon
  // with two items - 'Restore app' and 'Quit app'
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Restore app',
      click: () => {
        mainWindow.show()
      }
    },
    {
      label: 'Quit app',
      click: () => {
        mainWindow.close()
      }
    }
  ])

  // Set title for tray icon
  appIcon.setTitle('Grease the Groove')

  // Set toot tip for tray icon
  appIcon.setToolTip('Grease the Groove')

  // Create RightClick context menu
  appIcon.setContextMenu(contextMenu)

  // Restore (open) the app after clicking on tray icon
  // if window is already open, minimize it to system tray
  appIcon.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
  })

  // and load the index.html of the app.
  let indexPath

  // Setup for Webpack
  if (dev && process.argv.indexOf('--noDevServer') === -1) {
    indexPath = url.format({
      protocol: 'http:',
      host: 'localhost:8080',
      pathname: 'index.html',
      slashes: true
    })
  } else {
    indexPath = url.format({
      protocol: 'file:',
      pathname: path.join(__dirname, 'dist', 'index.html'),
      slashes: true
    })
  }

  mainWindow.loadURL(indexPath)

  // Don't show until we are ready and loaded
  mainWindow.once('ready-to-show', () => {
    mainWindow.show()

    // Open the DevTools automatically if developing
    if (dev) {
      mainWindow.webContents.openDevTools()
    }
  })

  // Emitted when the window is closed.
  mainWindow.on('closed', function() {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })

  // Minimize window to system tray
  mainWindow.on('minimize',function(event){
      event.preventDefault()
      mainWindow.hide()
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})

Příprava index.js

Další soubor, který bude nutný ke spuštění naší elektronové aplikace je index.js . Tento soubor bude uvnitř src adresář, který je v kořenovém adresáři tohoto projektu. Uvnitř tohoto souboru uděláme dvě věci. Nejprve vytvoříme div prvek, uvnitř kterého vykreslíme hlavní komponentu React naší elektronové aplikace. Pamatujte, že nepoužíváme statickou HTML šablonu. Webpack udělá těžkou práci a vytvoří tuto šablonu za nás. Takže se o to nemusíme starat v žádné fázi vývoje.

Pak je tu druhá věc, kterou uděláme. Naimportujeme React knihovna a render metoda z React-dom knihovna. A poté importujeme hlavní komponentu pro naši elektronovou aplikaci. Nazvěme tuto komponentu jednoduše App a vložíme to do App.jsx soubory uvnitř app adresář. Tento adresář bude uvnitř src . S tím můžeme použít render metoda k vykreslení našeho App součást uvnitř div dříve jsme vytvořili.

// Import React
import React from 'react'

// Import React-dom
import { render } from 'react-dom'

// Import the main App component
import App from './app/App'

// Since we are using HtmlWebpackPlugin WITHOUT a template
// we should create our own root node in the body element before rendering into it
let root = document.createElement('div')

// Add id to root 'div'
root.id = 'root'

// Append 'root' div to the 'body' element
document.body.appendChild(root)

// Render the main component of our electron application into the 'root' div
render(<App />, document.getElementById('root'))

Dovolte mi, abych vám ukázal strukturu složek, kterou jsme probrali ve druhé části. Hodně nám to usnadní pochopení a omotání hlavy. Takže znovu, adresáře a soubory, se kterými v tomto monentu pracujeme, jsou src/ , app/ App.jsx a index.js .

grease-the-groove-app
├── builds/
├── dist/
├── node_modules/
├── src/
│   └── app/
│       └── components/
│       └── App.jsx
│   └── assets/
│       └── grease-the-groove-icon.ico
│       └── grease-the-groove-icon.png
│       └── grease-the-groove-icon.svg
│   └── index.js
├── .babelrc
├── .editorconfig
├── main.js
├── package.json
├── README.md
├── webpack.build.config.js
├── webpack.dev.config.js
└── yarn.lock

Mějte na paměti, že tyto soubory, main.js a index.js jsou nezbytné pro provoz naší elektronové aplikace. Pokud se rozhodnete změnit názvy některého z těchto souborů nebo umístění, nezapomeňte také aktualizovat své konfigurace Webpack, webpack.build.config.js a webpack.dev.config.js .

Naše první a hlavní součást

Dobře. Všechny závislosti, které potřebujeme, jsou na místě. Připraveny jsou také konfigurace a workflow. Nyní je připraven i Electron. Pojďme tedy vytvořit první komponentu React pro naši elektronovou aplikaci. Toto bude App komponentu, o které jsme mluvili výše, a vložíme ji do src/app/App.jsx soubor. Nejprve naimportujeme React knihovna. Dále můžeme připravit další import pro komponentu Timer. Protože tuto komponentu ještě nemáme připravenou, pojďme ji okomentovat.

Následuje samotná součást. V této komponentě chceme použít stav aplikace. Z tohoto důvodu použijeme JavaScript class a vytvořit stavovou komponentu. V horní části komponenty bude constructor metoda s state vnořený uvnitř. State bude obsahovat čtyři klíče. První dva, isSettingsOpen a isTimerShown , bude logická hodnota a obě budou false jako výchozí. Tyto klíče použijeme k určení, zda zobrazit/skrýt Timer a zda otevřít/zavřít okno nastavení.

Druhý pár klíčů, numOfSets a restPauseLength budou obě celá čísla. Použijeme je k uložení počtu sérií, které chce uživatel udělat, a délky odpočinkové pauzy, kterou chce mít mezi sadami. Až skončíme s constructor a state , můžeme vytvořit jednoduchou metodu pro generování seznamu položek, kde každá položka bude představovat jednu sadu, kterou chce uživatel udělat. Všechny položky budou obsahovat checkbox a span (pro text) zabalený uvnitř label .

V tomto seznamu budeme používat for smyčka a numOfSets klíč z aplikace state , aby se vygenerovalo množství sad uživatelů zadaných v nastavení. Uvnitř vložíme každou z těchto položek seznamu do pole, které pak vrátíme a vykreslíme. Poté vytvoříme dvě velmi jednoduché metody toggleSettings pro otevření/zavření okna nastavení a toggleTimer pro zobrazení/skrytí Timer komponent. Každá z těchto metod změní isSettingsOpen a isTimerShown klíče v aplikaci state prostřednictvím setState metoda.

Dále vytvoříme další dvě jednoduché metody, updateNumOfSets a updateRestPauseLength . Tyto dva také změní konkrétní klíče v aplikaci state , numOfSets a restPauseLength prostřednictvím setState metoda. Jsme téměř hotovi. Poslední věc, kterou potřebujeme, abychom naši elektronovou aplikaci zprovoznili, je vytvoření uživatelského rozhraní a jeho vložení do render metoda. Prozatím dáme většinu částí uživatelského rozhraní do tohoto souboru. Můžeme to později přefaktorovat a rozdělit na menší komponenty.

O uživatelském rozhraní. Bude to poměrně jednoduché. Vytvoříme jeden hlavní nadpis, nějaký doplňkový text, jedno tlačítko pro otevření nastavení a jedno tlačítko pro zobrazení časovače a seznamu se sadami, které je třeba udělat. Pole nastavení bude obsahovat dva číselné vstupy, jeden pro zadání počtu sad a jeden pro zadání délky přestávky. Pro každý z těchto vstupů budou také nějaké další. Výsledek bude vypadat takto.

// Import React library
import React from 'react'

// Import timer (not implemented yet)
// import Timer from './components/Timer'

// Create the main component for our electron app
class App extends React.Component {
  constructor() {
    super()

    // Create and setup the app state
    this.state = {
      isSettingsOpen: false,
      isTimerShown: false,
      numOfSets: 6,
      restPauseLength: 90
    }
  }

  // Create a method for generating list of items, one for each set the user wants to do
  // each item will contain checkbox and label
  generateSetsList() {
    // Prepare empty array for list items
    let setsItems = []

    // Generate number of list items based on 'numOfSets'
    for(let i = 0; i<this.state.numOfSets; i++) {
      setsItems.push(<li key={i}>
        <label htmlFor={`set${i}`}>
          <input id={`set${i}`} name={`set${i}`} type="checkbox"/>

          <span>Set number {i+1}</span>
        </label>
      </li>)
    }

    // Return the array with list items
    return setsItems
  }

  // Create a method to open/close collapsible div with options
  toggleSettings(e) {
    e.preventDefault()

    // Change specific keys in app state to either open settings or show timer
    this.setState({
      isSettingsOpen: !this.state.isSettingsOpen,
      isTimerShown: false
    })
  }

  // Create a method to show/hide collapsible div with timer
  toggleTimer(e) {
    e.preventDefault()

    // Change specific keys in app state to either show timer or open settings
    this.setState({
      isSettingsOpen: false,
      isTimerShown: !this.state.isTimerShown
    })
  }

  // Create a method to update the 'numOfSets' key stored inside app state
  updateNumOfSets(e) {
    this.setState({
      numOfSets: e.target.value
    })
  }

  // Create a method to update the 'restPauseLength' key stored inside app state
  updateRestPauseLength(e) {
    this.setState({
      restPauseLength: e.target.value
    })
  }

  // Create the main render method
  render() {
    return (
      <div>
        <h1>Grease the Groove!</h1>

        <p>Are you ready to get stronger?</p>

        {/* Button to open/close the settings div */}
        <a href="#" onClick={(e) => this.toggleSettings(e)}>Settings</a>

        {/* Button to show/hide the Timer */}
        <a href="#" onClick={(e) => this.toggleTimer(e)}>Timer</a>

        {/* If the value of `isSettingsOpen` is true, open settings. */}
        {this.state.isSettingsOpen && <div className="settings">
          <p>How many sets do you want to do?</p>

          {/* Number input to let the user specify the number of sets he wants to do in a day. */}
          <input type="number" placeholder={this.state.numOfSets} onChange={(e) => this.updateNumOfSets(e)} />

          <p>How long should the rest pause be (in minutes)? You can use decimal numbers for seconds, i.e.: 0.2 for 12s.</p>

          {/* Number input to let the user specify the rest pause between sets. */}
          <input type="number" value={this.state.restPauseLength} onChange={(e) => this.updateRestPauseLength(e)} />
        </div>}

        {/* If the value of `isTimerShown` is true, show timer */}
        {/* and provide the timer with data about the length of the rest pause,
        stored inside app state via 'pauseLength' prop */}
        {/* Timer is not implemented yet */}
        {/* this.state.isTimerShown && <Timer pauseLength={this.state.restPauseLength} /> */}

        {/* Create list of sets to do */}
        <ul>
          {this.generateSetsList()}
        </ul>
      </div>
    )
  }
}

// Export the main component
export default App

A to je vše, co potřebujeme, než budeme moci spustit naši elektronovou aplikaci. Pojďme se tedy konečně podívat na výsledky naší práce a poprvé ji spustit. Můžeme spustit „dev“ verzi naší elektronové aplikace pomocí yarn run dev nebo npm run dev .

Závěrečné úvahy o budování elektronové aplikace

Gratulujeme! Právě jste dokončili třetí díl této mini série. A co je ještě důležitější, konečně jste měli první šanci aplikaci skutečně spustit a vidět plody své práce. Tedy pokud se nestalo něco neočekávaného. V takovém případě kód znovu zkontrolujte a ujistěte se, že v něm není překlep. Pokud to nepomůže, zkontrolujte, zda je struktura vašeho projektu správná a zda jste nainstalovali všechny závislosti. CMD a konzole pomohou vyřešit většinu problémů. Pokud problém přetrvává, dejte mi vědět.

Co bude naším úkolem ve čtvrté části? Pár věcí. Nejprve vytvoříme komponentu pro Timer a implementujeme ji. Dále vytvoříme další komponentu pro vizuální i zvuková upozornění. Poté budeme pracovat na stylech a vylepšit uživatelské rozhraní. Do té doby se připravte, protože budeme mít spoustu práce.