Developing apps with React & Electron

Developing apps with React & Electron

Code walkthrough of FocusedTask https://github.com/RStankov/FocusedTask as a showcase for React & Electron application.

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

October 27, 2020
Tweet

Transcript

  1. Developing desktop apps with React & Electron Radoslav Stankov 28/08/2020

  2. Radoslav Stankov @rstankov blog.rstankov.com
 twitter.com/rstankov
 github.com/rstankov
 speakerdeck.com/rstankov

  3. None
  4. None
  5. https://speakerdeck.com/rstankov

  6. https://github.com/RStankov/FocusedTask

  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. (main) (render)

  15. (main)

  16. (main) (render) rpc

  17. (main) (render) rpc

  18. const electron = window.require('electron') function closeApp() { electron.remote.getCurrentWindow().close(); } (render)

  19. const electron = window.require('electron') electron.ipcRenderer.send('event', arg1, arg2); (render)

  20. const electron = require('electron'); electron.ipcMain.on('event', function(_e, arg1, arg) { //

    ... }); (main)
  21. None
  22. None
  23. None
  24. None
  25. Focused Task https://github.com/RStankov/FocusedTask

  26. None
  27. None
  28. ! Tech Stack

  29. ! Tech Stack

  30. None
  31. None
  32. 3 package.json! "

  33. None
  34. shell/package.json app/package.json ./package.json distribution (shell) (app)

  35. https://github.com/maxogden/menubar

  36. const { menubar } = require('menubar'); const electron = require('electron');

    const mb = menubar({ index: process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, './build/index.html'), protocol: 'file:', slashes: true, }), icon: path.join(__dirname, 'assets/MenuBarIconTemplate.png'), browserWindow: { width: 500, height: 600, minWidth: 300, maxHeight: 900, minHeight: 600, webPreferences: { nodeIntegration: true, scrollBounce: true, }, }, }); mb.on('after-create-window', () => { // ... });
  37. const { menubar } = require('menubar'); const electron = require('electron');

    const mb = menubar({ index: process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, './build/index.html'), protocol: 'file:', slashes: true, }), icon: path.join(__dirname, 'assets/MenuBarIconTemplate.png'), browserWindow: { width: 500, height: 600, minWidth: 300, maxHeight: 900, minHeight: 600, webPreferences: { nodeIntegration: true, scrollBounce: true, }, }, }); mb.on('after-create-window', () => { // ... });
  38. create-react-app

  39. Development ./bin/react-start ELECTRON_START_URL=http://localhost:3000 ./bin/electron-start

  40. Distribution ./bin/react-build ./bin/electron-package FocusedTask.app

  41. None
  42. Access Electron via React

  43. Access Electron via React

  44. Access Electron via React

  45. Access Electron via React

  46. Open external links

  47. Open external links mb.app.on('web-contents-created', (e, contents) => { const openExternal

    = (e, url) => { e.preventDefault(); electron.shell.openExternal(url); }; contents.on('new-window', openExternal); contents.on('will-navigate', (e, url) => { if (url !== contents.getURL()) { openExternal(e, url); } }); });
  48. Open external links export function openURI(uri: string) { if (!isElectron)

    { return; } if (!isURI(uri)) { return; } if (isFilePathUri(uri)) { electron.remote.shell.openItem(uri); } else { electron.remote.shell.openExternal(uri); } }
  49. Open external links import * as React from 'react'; import

    { openURI } from 'utils/electron'; interface IProps { children: React.ReactNode; href: string; className?: string; } export default function ExternalLink(props: IProps) { return ( <a target="_blank" rel="noopener noreferrer" onClick={e => { e.preventDefault(); openURI(props.href); }} {...props} /> ); }
  50. Resize window to fit content

  51. Resize window to fit content export function resizeBasedOnContent() { if

    (!isElectron) { return; } const bodyStyle = window.getComputedStyle(document.body) as any; const padding = parseInt(bodyStyle['margin-top'], 10) + parseInt(bodyStyle['margin-bottom'], 10) + parseInt(bodyStyle['padding-top'], 10) + parseInt(bodyStyle['padding-bottom'], 10); const observer = new ResizeObserver(() => { const height = Math.min(900, document.body.offsetHeight + padding); const bounds = electron.remote .getCurrentWindow() .webContents.getOwnerBrowserWindow() .getBounds(); if (bounds.height === height) { return; } electron.ipcRenderer.send('resize', bounds.width, height); }); observer.observe(document.body); }
  52. Resize window to fit content electron.ipcMain.on('resize', function(_e, width, height) {

    mb.window.setSize(width, height); });
  53. Store window position

  54. Store window position const settings = require('electron-settings'); const electron =

    require('electron'); const _ = require('lodash'); const BOUNDS_KEY = 'windowBounds'; module.exports = { setWindowBounds(menubar) { menubar.setOption('browserWindow', { ...menubar.getOption('browserWindow'), ...(settings.get(BOUNDS_KEY) || {}), }); }, trackWindowBounds(menubar) { const win = menubar.window; const handler = _.debounce(() => { menubar.setOption('browserWindow', { ...menubar.getOption('browserWindow'), ...win.getBounds(), }); settings.set(BOUNDS_KEY, win.getBounds()); }, 500); win.on('resize', handler); win.on('move', handler); }, };
  55. Store window position mb.on('after-create-window', () => { settings.trackWindowBounds(mb); }); mb.app.on('ready',

    () => { settings.setWindowBounds(mb); });
  56. Global shortcut

  57. None
  58. None
  59. Global shortcut export function getGlobalShortcutKey() { if (!isElectron) { return;

    } return electron.remote.getGlobal('globalShortcutKey'); } export function updateGlobalShortcutKey(key: string) { if (!isElectron) { return; } if (key.length !== 1) { return; } electron.ipcRenderer.send('updateGlobalShortcutKey', key); }
  60. Global shortcut mb.app.on('ready', () => { settings.setGlobalShortcut(mb); }); mb.app.on('will-quit', ()

    => { electron.globalShortcut.unregisterAll(); }); electron.ipcMain.on('updateGlobalShortcutKey', (_e, key) => { settings.updateGlobalShortcutKey(mb, key); });
  61. Global shortcut const settings = require('electron-settings'); const electron = require('electron');

    const _ = require('lodash'); module.exports = { setGlobalShortcut(menubar) { const key = settings.get(SHORTCUT_KEY) || "'"; global.globalShortcutKey = key; electron.globalShortcut.register(`CommandOrControl+${key}`, () => { if (menubar.window && menubar.window.isVisible()) { menubar.hideWindow(); } else { menubar.showWindow(); } }); }, updateGlobalShortcutKey(menubar, key) { settings.set(SHORTCUT_KEY, key); electron.globalShortcut.unregisterAll(); this.setGlobalShortcut(menubar); }, };
  62. Changelog

  63. None
  64. None
  65. Changelog import React from 'react'; import Stack from 'components/Stack'; import

    BackButton from 'components/BackButton'; import Title from 'components/Title'; import ReactMarkdown from 'react-markdown'; import styles from './styles.module.css'; import raw from 'raw.macro'; const markdown = raw('../../../../CHANGELOG.md').replace(/^# .*\n\n/, ''); export default function Shortcuts() { return ( <> <BackButton /> <Stack.Column> <Title emoji="#" title="Changelog" /> <div className={styles.markdown}> <ReactMarkdown source={markdown} escapeHtml={true} /> </div> </Stack.Column> </> ); }
  66. Distribution

  67. const packager = require('electron-packager'); const setLanguages = require('electron-packager-languages'); const createDMG

    = require('electron-installer-dmg'); const version = '0.1.0'; const distPath = './dist'; const name = 'FocusedTask'; packager({ dir: './shell', overwrite: true, out: distPath, afterCopy: [setLanguages(['en', 'en_GB'])], name, productName: 'Focused Task', appBundleId: 'com.rstankov.focused-task', appCopyright: 'Copyright (C) 2020 Radoslav Stankov', appVersion: version, appCategoryType: 'public.app-category.productivity', // osxSign: { // identity: '[?]', // 'hardened-runtime': true, // entitlements: 'entitlements.plist', // 'entitlements-inherit': 'entitlements.plist', // 'signature-flags': 'library', // }, // osxNotarize: { // appleId: '[?]', // appleIdPassword: '[?]', // }, }).then(() => {
  68. None
  69. None
  70. ...payment was rejected [N] times $

  71. ...turns out you need a credit card %

  72. &

  73. const packager = require('electron-packager'); const setLanguages = require('electron-packager-languages'); const createZip

    = require('electron-installer-zip'); const version = require('../shell/package.json').version; const credentials = require('../credentials.json'); packager({ dir: './shell', overwrite: true, out: './dist', afterCopy: [setLanguages(['en', 'en_GB'])], productName: 'FocusedTask', productName: 'Focused Task', appBundleId: 'com.rstankov.focused-task', appCopyright: 'Copyright (C) 2020 Radoslav Stankov', appVersion: version, appCategoryType: 'public.app-category.productivity', icon: './assets/Icon.icns', osxSign: { identity: credentials.identity, 'hardened-runtime': true, entitlements: 'entitlements.plist', 'entitlements-inherit': 'entitlements.plist', 'signature-flags': 'library', }, osxNotarize: credentials.osxNotarize, });
  74. Auto update

  75. None
  76. const autoUpdate = require('./utils/autoupdate'); // ... mb.app.on('ready', () => {

    // ... if (!isDev) { autoUpdate(); } });
  77. https://github.com/RStankov/FocusedTask/blob/master/shell/utils/autoupdate.js

  78. None
  79. Thanks ' https://speakerdeck.com/rstankov https://github.com/RStankov/FocusedTask (