Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Electron - How and Why

Electron - How and Why

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

Avatar for David Tanzer

David Tanzer

May 17, 2022
Tweet

More Decks by David Tanzer

Other Decks in Programming

Transcript

  1. 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/
  2. 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
  3. For Me For Me For Me For Me For Me

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

    video, audio, ...) Auto-reload React, Vue, Angular, ... Developer tools and developer experience @dtanzer 8
  5. 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
  6. 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
  7. 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
  8. Electron Forge Electron Forge Electron Forge Electron Forge Electron Forge

    npx create-electron-app my-new-app \ --template=typescript-webpack @dtanzer 17
  9. 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
  10. 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
  11. And the React Code? And the React Code? And the

    React Code? And the React Code? And the React Code? @dtanzer 20
  12. 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
  13. 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
  14. 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
  15. Reloading the Application Reloading the Application Reloading the Application Reloading

    the Application Reloading the Application Webpack: Reloads automatically Otherwise: type "rs" @dtanzer 24
  16. 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
  17. 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
  18. Security Security Security Security Security Expose all node APIs to

    renderers (insecure) -or- Expose an API @dtanzer 29
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. Building / Publishing Building / Publishing Building / Publishing Building

    / Publishing Building / Publishing With tools / scripts from electron-forge Configured in package.json @dtanzer 47
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Result out \-- make +-- marmota-22.19.0-arm64.dmg \-- zip \-- darwin

    \-- arm64 \-- marmota-darwin-arm64-22.19.0.zip @dtanzer 54
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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/