$30 off During Our Annual Pro Sale. View Details »

Electron - How and Why

Electron - How and Why

In this talk, I show how to develop desktop applications using Electron.

David Tanzer

May 17, 2022
Tweet

More Decks by David Tanzer

Other Decks in Programming

Transcript

  1. Electron: How and Why @dtanzer 1

  2. Build cross-platform desktop apps with JavaScript, HTML, and CSS If

    you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application. -- https://www.electronjs.org/
  3. Today: Why Electron Marmota.app Getting Started Concepts Publishing your App

    This Presentation: Speakerdeck: https://speakerdeck.com/dtanzer/ marmota.app presentation (Markdown): https://marmota.app/blog/talk-electron/ @dtanzer 3
  4. Why Electron @dtanzer 4

  5. Advantages Advantages Advantages Advantages Advantages Windows, Linux, MacOS Auto-update is

    "free" Somtimes "Most economical option" @dtanzer 5
  6. Downsides Downsides Downsides Downsides Downsides App Size Resource Usage Native

    look & Feel (possible, but can be hard) @dtanzer 6
  7. For Me For Me For Me For Me For Me

    "Power of the web" TypeScript JavaScript Ecosystem @dtanzer 7
  8. "Power of the Web" HTML and CSS Browser capabilities (animations,

    video, audio, ...) Auto-reload React, Vue, Angular, ... Developer tools and developer experience @dtanzer 8
  9. TypeScript const v: string 5 = @dtanzer 9

  10. export type ChannelToMain = { '--presentations--get-screen-sources': () => void, }

    export interface PresentationsApi { getScreenSources: ChannelToMain[ '--presentations--get-screen-sources'], } export const presentations: PresentationsApi = { getScreenSources: () => rendererIpc.sendAsync( '--presentations--get-screen-sources'), } @dtanzer 10
  11. type HasWon<G extends OngoingGame<any>, P extends Player> = { T:

    'F' F: Or< And<IsPlayer<G, 'TOP', 'LEFT', P>, And<IsPlayer<G, 'TOP', 'CENTER', P> Or<And<IsPlayer<G, 'MIDDLE', 'LEFT', P>, And<IsPlayer<G, 'MIDDLE', 'CE Or<And<IsPlayer<G, 'BOTTOM', 'LEFT', P>, And<IsPlayer<G, 'BOTTOM', 'CE Or<And<IsPlayer<G, 'TOP', 'LEFT', P>, And<IsPlayer<G, 'MIDDLE', 'LEFT' Or<And<IsPlayer<G, 'TOP', 'CENTER', P>, And<IsPlayer<G, 'MIDDLE', 'CEN Or<And<IsPlayer<G, 'TOP', 'RIGHT', P>, And<IsPlayer<G, 'MIDDLE', 'RIGH /* ... */ > }[G['isEmpty']] https://github.com/dtanzer/typetactoe/blob/master/src/tictactoe.ts @dtanzer 11
  12. JavaScript Ecosystem @dtanzer 12

  13. marmota.app @dtanzer 13

  14. Demo Time marmota.app Presentation Software @dtanzer 14

  15. My First Electron Application My First Electron Application My First

    Electron Application My First Electron Application My First Electron Application For a hardware company Visualization and optimization @dtanzer 15
  16. Getting Started @dtanzer 16

  17. Electron Forge Electron Forge Electron Forge Electron Forge Electron Forge

    npx create-electron-app my-new-app \ --template=typescript-webpack @dtanzer 17
  18. index.js index.js index.js index.js index.js Menu.setApplicationMenu(buildApplicationMenu()) app.on('ready', () => {

    registerProtocol() const mainWindow = createMainWindow() registerWindow('main', mainWindow) }) @dtanzer 18
  19. index.html index.html index.html index.html index.html <!DOCTYPE html> <html> <head> <title>marmota.app

    - Simple Markdown Presentations</title> <script> //ugly hack for react-shadow, which accesses the "global" //variable global = globalThis </script> </head> <body> <div id="react-root"></div> </body> </html> @dtanzer 19
  20. And the React Code? And the React Code? And the

    React Code? And the React Code? And the React Code? @dtanzer 20
  21. package.json [ "@electron-forge/plugin-webpack", { "renderer": { "entryPoints": [ { "html":

    "./src/index.html", "js": "./src/renderer.tsx", "name": "main_window", "preload": { "js": "./src/preload.ts" } } ] } }] @dtanzer 21
  22. renderer.ts if(w.location.search.indexOf('mode=presentation')>=0) { ReactDOM.render(<PresentationWindow />, reactRoot) } else if(w.location.search.indexOf('mode=presenter-screen')>=0) {

    ReactDOM.render(<PresenterScreenWindow />, reactRoot) } else if(w.location.search.indexOf('mode=export-or-print')>=0) { ReactDOM.render(<ExportOrPrintWindow />, reactRoot) } else { const { documentsModel, tabsModel } = createModel() ReactDOM.render( <MainWindow documentsModel={documentsModel} tabsModel={tabsModel} />, reactRoot) } @dtanzer 22
  23. Developer Tools Developer Tools Developer Tools Developer Tools Developer Tools

    app.on('ready', () => { /* ... */ if(isDevelopment()) { installExtension(REACT_DEVELOPER_TOOLS) .then((name) => console.log(`Added Extension: ${name}`)) .catch((err) => console.log('An error occurred: ', err)) } }) @dtanzer 23
  24. Reloading the Application Reloading the Application Reloading the Application Reloading

    the Application Reloading the Application Webpack: Reloads automatically Otherwise: type "rs" @dtanzer 24
  25. Demo Time "my-new-app" @dtanzer 25

  26. Concepts @dtanzer 26

  27. Main and Renderer Processes Main and Renderer Processes Main and

    Renderer Processes Main and Renderer Processes Main and Renderer Processes main process renderer processes @dtanzer 27
  28. Browser Windows Browser Windows Browser Windows Browser Windows Browser Windows

    const titleBarStyle = process.platform==='darwin'?'hidden':undefined const window = new BrowserWindow({ ...options, titleBarStyle, }) window.once('ready-to-show', () => { window.show() }) window.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}${modeParameter}`) window.webContents.on("did-frame-finish-load", () => { if(isDevelopment()) { window.webContents.openDevTools() } }) @dtanzer 28
  29. Security Security Security Security Security Expose all node APIs to

    renderers (insecure) -or- Expose an API @dtanzer 29
  30. Expose an API: preload.js import { contextBridge, ipcRenderer, } from

    'electron' import { api } from '$main/mainapi/api' import { AppApi } from '$api/app/types' import { registerRendererApiCallbacks } from './mainapi/renderer' contextBridge.exposeInMainWorld('api', api) registerRendererApiCallbacks() @dtanzer 30
  31. Expose an API: api.js import { app, } from './app/renderer'

    import { action, } from './action/renderer' /* ... */ export const api = { app, action, /* ... */ } type Api = typeof api declare global { interface Window { api: Api } } @dtanzer 31
  32. Expose an API: Usage export type ExportOrPrintWindowProperties = { /*

    ... */ _getPlatform?: typeof window.api.app.getPlatform, } export function ExportOrPrintWindow({ /* ... */ _getPlatform = window.api.app.getPlatform, }: ExportOrPrintWindowProperties): React.ReactElement { const macTitleBar = _getPlatform() === 'darwin'? <MacTitleBar /> : undefined /* ... */ } @dtanzer 32
  33. Node APIs Node APIs Node APIs Node APIs Node APIs

    function savePresentationAs(rawPresentation: string) { const filePath = dialog.showSaveDialogSync({ title: 'Save Presentation As', }) if(filePath) { fs.writeFileSync(filePath, rawPresentation, 'utf-8') return { savedFilePath: filePath, savedBasePath: path.dirname(filePath), savedFileName: path.basename(filePath), } } return undefined } @dtanzer 33
  34. Application Menu Application Menu Application Menu Application Menu Application Menu

    Menu.setApplicationMenu(buildApplicationMenu()) export function buildApplicationMenu(): Menu { const aboutMenuItem = new MenuItem({ click: (_, browserWindow, __) => { sendActionEventToWindow('application.about', /* ... */) }, label: 'About', }) /* ... */ helpMenu.submenu?.append(aboutMenuItem) menu.append(helpMenu) return menu } @dtanzer 34
  35. IPC IPC IPC IPC IPC ipcMain IPC-Object, used in the

    main process ipcRenderer IPC-Object, used in the renderer processes import { ipcRenderer } from "electron"; ipcRenderer.sendSync('some event', ...args) ipcMain.on(channel, ('some event', ...args) => { console.log('recevied event') } @dtanzer 35
  36. How I do IPC How I do IPC How I

    do IPC How I do IPC How I do IPC Two type-safe helper classes Data Types shared by main and renderer processes Other IPC-Code split into main.ts and renderer.ts @dtanzer 36
  37. IPC: Shared Data Types export type ChannelToMain = { '--annotations--publish-added-annotation':

    /* function */, } export type ChannelToRenderer = { '--annotations--annotation-published': /* function */, } export interface AnnotationsApi { publishAddedAnnotation: ChannelToMain[ '--annotations--publish-added-annotation'], onAnnotationAdded: (callback: (annotation: SerializedAnnotation) => unknown) => void, }``` @dtanzer 37
  38. IPC: renderer.ts export const annotations: AnnotationsApi = { publishAddedAnnotation: (

    w: AnnotationWindowName, a: SerializedAnnotation ) => rendererIpc.sendAsync( '--annotations--publish-added-annotation', w, a), onAnnotationAdded: ( callback: (annotation: SerializedAnnotation) => unknown ) => { annotationAddedCallback = callback }, } rendererIpc.on({ '--annotations--annotation-published': ( serialized: SerializedAnnotation ) => annotationAddedCallback?.(serialized), }) @dtanzer 38
  39. IPC: main.ts export function registerMainAnnotationsCallbacks() { mainIpc.on({ '--annotations--publish-added-annotation': publishAddedAnnotation, })

    } function publishAddedAnnotation(/* ... */) { /* ... */ } @dtanzer 39
  40. Webpack Webpack Webpack Webpack Webpack Monaco Editor Using Imports Resolve

    Configuration @dtanzer 40
  41. resolve Configuration resolve Configuration resolve Configuration resolve Configuration resolve Configuration

    import { getWindow } from "$api/app/main" webpack.resolve.js module.exports = { resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], fallback: { path: false, fs: false, }, alias: { '$api': path.resolve(__dirname, 'src', 'mainapi'), }, }, } @dtanzer 41
  42. Resolve: Jest const config: Config.InitialOptions = { moduleNameMapper: { "^\\$api(.*)$":

    "<rootDir>/src/mainapi$1", "^(.*)/MonacoEditor$": "<rootDir>/__mocks__/reactMonaco.js", "^monaco-editor(.*)$": "<rootDir>/__mocks__/monacoMock.js", } }; @dtanzer 42
  43. Resolve: tsconfig.json "paths": { "$api/*": ["src/mainapi/*"] } @dtanzer 43

  44. Content Security Policy (CSP) Content Security Policy (CSP) Content Security

    Policy (CSP) Content Security Policy (CSP) Content Security Policy (CSP) index.html <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' assets:; img-src 'self' data: assets: presentation: design:; font-src 'self' assets: design:"> @dtanzer 44
  45. CSP: package.json "devContentSecurityPolicy": "default-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' assets:;

    img-src 'self' data: assets: presentation: design:; font-src 'self' assets:", @dtanzer 45
  46. Publishing your App @dtanzer 46

  47. Building / Publishing Building / Publishing Building / Publishing Building

    / Publishing Building / Publishing With tools / scripts from electron-forge Configured in package.json @dtanzer 47
  48. Building for Windows Building for Windows Building for Windows Building

    for Windows Building for Windows "makers": [ { "name": "@electron-forge/maker-squirrel", "config": { "remoteReleases": "https://download.marmota.app/win32", "name": "marmota.app", "certificateFile": "build/evcodesign.pfx", "certificatePassword": "authenticode-certificate-password", "setupIcon": "./assets/images/marmota.ico" } }, ... ] @dtanzer 48
  49. Certificate Password #!/bin/sh echo Please enter the Authenticode certificate password

    read certificate_password sed -i -e "s/\"certificatePassword\": \"authenticode-certificate-... npm run gather-licenses npm run make git checkout package.json @dtanzer 49
  50. Result out \-- make \-- squirrel.windows \-- x64 +-- RELEASES

    +-- marmota-22.19.0 Setup.exe +-- marmota-22.17.0-full.nupkg +-- marmota-22.19.0-delta.nupkg \-- marmota-22.19.0-full.nupkg @dtanzer 50
  51. Installing from EXE Download and double-click. @dtanzer 51

  52. Building for Mac Building for Mac Building for Mac Building

    for Mac Building for Mac "packagerConfig": { "osxSign": { "identity": "[Developer ID]", "hardened-runtime": true, "entitlements": "entitlements.plist", "entitlements-inherit": "entitlements.plist", "signature-flags": "library" }, "osxNotarize": { "appleId": "[Apple ID]", "appleIdPassword": "appleid-app-password", "ascProvider": "[ASC Provider]" }, "icon": "./assets/images/marmota", "appBundleId": "app.marmota.presentations" } @dtanzer 52
  53. Maker Config "makers": [ { "name": "@electron-forge/maker-dmg", "config": { "format":

    "ULFO", "background": "./assets/images/dmg-background.png" } }, { "name": "@electron-forge/maker-zip", "platforms": [ "darwin" ] }, ... ] @dtanzer 53
  54. Result out \-- make +-- marmota-22.19.0-arm64.dmg \-- zip \-- darwin

    \-- arm64 \-- marmota-darwin-arm64-22.19.0.zip @dtanzer 54
  55. Installing from DMG @dtanzer 55

  56. Building for Linux Building for Linux Building for Linux Building

    for Linux Building for Linux "makers": [ { "name": "@electron-forge/maker-deb", "config": { "options": { "icon": "./assets/images/marmota.png" } } }, { "name": "@electron-forge/maker-rpm", "config": { "options": { "icon": "./assets/images/marmota.png" } } } ], @dtanzer 56
  57. Installing from Package Manager sudo dnf config-manager --add-repo https://download.marmota... sudo

    rpm --import https://download.marmota.app/linux/rpm/... sudo dnf install marmota marmota 1. Add the repository 2. Import the public key 3. Install marmota.app 4. Run marmota.app @dtanzer 57
  58. Code Signing Code Signing Code Signing Code Signing Code Signing

    Windows: Authenticode "EV" certificate MacOS: Developer account, app specific password Linux: Any GPG key @dtanzer 58
  59. Auto-Update Auto-Update Auto-Update Auto-Update Auto-Update Windows, Mac: Application Startup Linux:

    Package Manager @dtanzer 59
  60. Application Code Application Code Application Code Application Code Application Code

    export function Updater(): React.ReactElement { const [ showDownloadUpdatePopup, setShowDownloadUpdatePopup, ]=... useEffect(() => { window.api.app.checkForUpdates(() => { setShowDownloadUpdatePopup(true) }) }, []) const downloadUpdatePopup = createDownloadUpdatePopup(...) return <> {downloadUpdatePopup} </> } @dtanzer 60
  61. function checkForUpdates(mainWindow: BrowserWindow | undefined) { try { autoUpdater.on('update-downloaded', (_:

    any) => { mainWindow?.webContents.send('--app--updates-available') }) let path : string = process.platform if(process.platform === 'darwin') { path += '.php?version=' ... } const url = 'https://download.marmota.app/' + path autoUpdater.setFeedURL({ url: url, }) autoUpdater.checkForUpdates() autoUpdater.on('error', (error: any) => {...}) autoUpdater.on('update-not-available', () => { ... }) } catch (e) { ... } } @dtanzer 61
  62. Windows: "Repository" Windows: "Repository" Windows: "Repository" Windows: "Repository" Windows: "Repository"

    Upload the output of make to an HTTP server: RELEASES marmota-22.19.0 Setup.exe marmota-22.17.0-full.nupkg marmota-22.19.0-delta.nupkg marmota-22.19.0-full.nupkg @dtanzer 62
  63. Mac: "Squirrel Server" Mac: "Squirrel Server" Mac: "Squirrel Server" Mac:

    "Squirrel Server" Mac: "Squirrel Server" Run the squirrel server from GitHup or write your own: <?php if ($_GET['version'] != "22.19.0") { header('Content-Type: application/json'); http_response_code(200); ?> { "name":"marmota.app 22.19.0", "notes":"", "url":"https://download.marmota.app/macOS/marmota-darwin-arm64-22.19.0.zip } <?php } else { http_response_code(204); } ?> @dtanzer 63
  64. ...From User's Perspective @dtanzer 64

  65. Linux: Package Manager Linux: Package Manager Linux: Package Manager Linux:

    Package Manager Linux: Package Manager Create package manager repositories Upload them to an HTTP server @dtanzer 65
  66. To Recap... To Recap... To Recap... To Recap... To Recap...

    @dtanzer 66
  67. About David About David About David About David About David

    Trainer, Coach, Developer https://davidtanzer.net @dtanzer This Presentation: Speakerdeck: https://speakerdeck.com/dtanzer/ marmota.app (Markdown): https://marmota.app/blog/talk-electron/