$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

    View Slide

  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/

    View Slide

  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

    View Slide

  4. Why Electron
    @dtanzer
    4

    View Slide

  5. Advantages
    Advantages
    Advantages
    Advantages
    Advantages
    Windows, Linux, MacOS
    Auto-update is "free"
    Somtimes "Most economical option"
    @dtanzer
    5

    View Slide

  6. Downsides
    Downsides
    Downsides
    Downsides
    Downsides
    App Size
    Resource Usage
    Native look & Feel (possible, but can be hard)
    @dtanzer
    6

    View Slide

  7. For Me
    For Me
    For Me
    For Me
    For Me
    "Power of the web"
    TypeScript
    JavaScript Ecosystem
    @dtanzer
    7

    View Slide

  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

    View Slide

  9. TypeScript
    const v: string 5
    =
    @dtanzer
    9

    View Slide

  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

    View Slide

  11. type HasWon, P extends Player> = {

    T: 'F'

    F: Or<

    And, And
    Or, AndOr, And

    Or, AndOr, AndOr, And/* ... */

    >

    }[G['isEmpty']]
    https://github.com/dtanzer/typetactoe/blob/master/src/tictactoe.ts
    @dtanzer
    11

    View Slide

  12. JavaScript Ecosystem
    @dtanzer
    12

    View Slide

  13. marmota.app
    @dtanzer
    13

    View Slide

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

    View Slide

  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

    View Slide

  16. Getting Started
    @dtanzer
    16

    View Slide

  17. Electron Forge
    Electron Forge
    Electron Forge
    Electron Forge
    Electron Forge
    npx create-electron-app my-new-app \

    --template=typescript-webpack
    @dtanzer
    17

    View Slide

  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

    View Slide

  19. index.html
    index.html
    index.html
    index.html
    index.html






    marmota.app - Simple Markdown Presentations

    <br/><br/>//ugly hack for react-shadow, which accesses the "global"<br/><br/>//variable<br/><br/>global = globalThis<br/><br/>










    @dtanzer
    19

    View Slide

  20. And the React Code?
    And the React Code?
    And the React Code?
    And the React Code?
    And the React Code?
    @dtanzer
    20

    View Slide

  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

    View Slide

  22. renderer.ts
    if(w.location.search.indexOf('mode=presentation')>=0) {

    ReactDOM.render(, reactRoot)

    } else if(w.location.search.indexOf('mode=presenter-screen')>=0) {

    ReactDOM.render(, reactRoot)

    } else if(w.location.search.indexOf('mode=export-or-print')>=0) {

    ReactDOM.render(, reactRoot)

    } else {

    const { documentsModel, tabsModel } = createModel()

    ReactDOM.render(


    tabsModel={tabsModel}

    />, reactRoot)

    }
    @dtanzer
    22

    View Slide

  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

    View Slide

  24. Reloading the Application
    Reloading the Application
    Reloading the Application
    Reloading the Application
    Reloading the Application
    Webpack: Reloads automatically
    Otherwise: type "rs"
    @dtanzer
    24

    View Slide

  25. Demo Time
    "my-new-app"
    @dtanzer
    25

    View Slide

  26. Concepts
    @dtanzer
    26

    View Slide

  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

    View Slide

  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

    View Slide

  29. Security
    Security
    Security
    Security
    Security
    Expose all node APIs to renderers (insecure)
    -or-
    Expose an API
    @dtanzer
    29

    View Slide

  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

    View Slide

  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

    View Slide

  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'?

    : undefined



    /* ... */

    }
    @dtanzer
    32

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  39. IPC: main.ts
    export function registerMainAnnotationsCallbacks() {

    mainIpc.on({

    '--annotations--publish-added-annotation':

    publishAddedAnnotation,

    })

    }



    function publishAddedAnnotation(/* ... */) {

    /* ... */

    }
    @dtanzer
    39

    View Slide

  40. Webpack
    Webpack
    Webpack
    Webpack
    Webpack
    Monaco Editor
    Using Imports
    Resolve Configuration
    @dtanzer
    40

    View Slide

  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

    View Slide

  42. Resolve: Jest
    const config: Config.InitialOptions = {

    moduleNameMapper: {

    "^\\$api(.*)$": "/src/mainapi$1",

    "^(.*)/MonacoEditor$": "/__mocks__/reactMonaco.js",

    "^monaco-editor(.*)$": "/__mocks__/monacoMock.js",

    }

    };
    @dtanzer
    42

    View Slide

  43. Resolve: tsconfig.json
    "paths": {

    "$api/*": ["src/mainapi/*"]

    }
    @dtanzer
    43

    View Slide

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

    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

    View Slide

  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

    View Slide

  46. Publishing your App
    @dtanzer
    46

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  51. Installing from EXE
    Download and double-click.
    @dtanzer
    51

    View Slide

  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

    View Slide

  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

    View Slide

  54. Result
    out

    \-- make

    +-- marmota-22.19.0-arm64.dmg

    \-- zip

    \-- darwin

    \-- arm64

    \-- marmota-darwin-arm64-22.19.0.zip
    @dtanzer
    54

    View Slide

  55. Installing from DMG
    @dtanzer
    55

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  59. Auto-Update
    Auto-Update
    Auto-Update
    Auto-Update
    Auto-Update
    Windows, Mac: Application Startup
    Linux: Package Manager
    @dtanzer
    59

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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:

    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
    }


    } else {

    http_response_code(204);

    }

    ?>
    @dtanzer
    63

    View Slide

  64. ...From User's Perspective
    @dtanzer
    64

    View Slide

  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

    View Slide

  66. To Recap...
    To Recap...
    To Recap...
    To Recap...
    To Recap...
    @dtanzer
    66

    View Slide

  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/

    View Slide