chalmery

chalmery

github

Electron(1)

Electron は、HTML と JS をデスクトップアプリケーションにパッケージ化することを可能にする JS デスクトップフレームワークであり、すでに多くのアプリケーションがこの技術を使用しています。例えば:vscode、notion、figma、思源ノートなど。

image-20230614231941-kvlx0yu

一 全体的なインタラクション形式#

デスクトップアプリケーションで最も重要なことは何でしょうか、あるいはブラウザのページと何が違うのでしょうか?大きく言えば、オペレーティングシステムとインタラクションできることです。では、オペレーティングシステムとインタラクションできるとはどういうことでしょうか?ブラウザを振り返ってみましょう。ブラウザは箱のようなもので、その中のすべてのページの権限は限られています。例えば、

  • ブラウザで自分が書いた HTML ページから JS 関数を使って、ドキュメントフォルダ内にファイルを作成したり、ファイルを変更したりすること
  • システム通知 API を呼び出して、システム通知を使用すること
  • システムの音量を変更すること?

これらはすべてできません。これらはオペレーティングシステムの API に関わることです。では、Electron はどのようにこれを実現しているのでしょうか?この図を見てみましょう:

bafkreihsovyhp2czleo53nzxtzlrmvbcq3tech4mzljwu2xnauk4sztahm

Electron は、JS にオペレーティングシステム API に曲線的にアクセスする能力を提供する中間層のようなものです。システムに関連する操作を行うには、すべて Electron を通じて行う必要があります。

簡単な例を挙げると、ファイル選択ダイアログを通じてファイルのパスを取得したい場合、どうすれば実現できるでしょうか?ブラウザでもファイルを選択できるので、Electron は必要ないと思うかもしれません。

問題ありません。ファイルを選択することはできますが、ファイルのパスを取得することはできません。普通のウェブページで、クリックするだけであなたのローカルファイルのパスや他のオペレーティングシステムの情報を取得できるとしたら、ブラウザの開発者としてそれを許可しますか?これはブラウザのユーザーにとって非常に危険です。ウェブページを開くだけで情報が漏洩し、さらにはコンピュータ上のファイルが失われる可能性があります。

したがって、使用するファイル選択ダイアログはオペレーティングシステムのファイル選択ダイアログであり、これは明らかに JS では実現できません。そこで Electron が登場します。Electron は次のような API を提供します。詳細は:https://www.electronjs.org/zh/docs/latest/api/dialog

import {dialog} from "electron";

function open() {
    let files = dialog.showOpenDialogSync({
        title: 'ファイルパスを選択',
        properties: ['openDirectory', 'multiSelections']
    })
    console.log(files)
    return files
}

ファイル選択ダイアログは用意できましたが、Electron にどのように通知すればよいのでしょうか?言い換えれば、上記の JS 関数は表示層からは呼び出せません。したがって、Electron は私たちが相互作用できる方法を提供しています。この方法は JS のパブリッシュ・サブスクライブモデルに似ており、イベントを発行し、消費者がそのイベントをリッスンします。

詳細は:https://www.electronjs.org/zh/docs/latest/api/ipc-renderer

JS 側は ipcRenderer.send を通じて Electron にイベントを送信します。

const electron = window.electron

electron.ipcRenderer.send('イベント名', data)

Electron 側は ipcMain.on を通じて JS 側から送信されたイベントをリッスンします。では、この処理が完了したとき、JS 側にどのように通知すればよいのでしょうか?event.reply を通じてイベントを送信し、データを JS に渡します。これは上記の ipcRenderer.send と似ています。

import {ipcMain} from "electron"

ipcMain.on('イベント名', (event, data) => {
    // 処理

    // コールバック
    event.reply('新しいイベント名', 返すデータ)
})

JS 側はどのように処理するのでしょうか?React の例を挙げると、useEffect 内で ipcRenderer.on を通じてこのイベントをリッスンし、処理します。この useEffect は、ページが作成されるとすぐにこの関数を実行し、return 内でこのイベントを削除します。そうしないと、ページが読み込まれるたびにリスナーが作成され、どんどん増えてしまいます。

useEffect(() => {
    // Electron側から送信されたイベントを処理
    electron.ipcRenderer.on('イベント名', (event, データ) => {
      // 処理
    });

    return () => {
      electron.ipcRenderer.removeAllListeners('イベント名');
    };
  }, []);

これでインタラクションが完了しました。とても簡単ですね。

二 パッケージング#

パッケージングは設定次第です。これが私の設定です:

/**
 * @see https://www.electron.build/configuration/configuration
 */
{
  appId: "electron-music",
  productName: "electron-music",
  copyright: "Copyright © 2023 ${author}",
  asar: true,
  directories: {
    output: "release",
    buildResources: "public"
  },
  files: [
    "dist"
  ],
  win: {
    icon: "public/icons/music256x256.png",
    target: [
      {
        target: "dir",
        arch: [
          "x64"
        ]
      },
      {
        target: "nsis",
        arch: [
          "x64"
        ]
      }
    ]
  },
  nsis: {
    "oneClick": false,
    "allowElevation": true,
    "allowToChangeInstallationDirectory": true,
    "installerIcon": "public/icons/music256x256.png",
    "uninstallerIcon": "public/icons/music256x256.png",
    "installerHeader": "public/icons/music256x256.png",
    "installerHeaderIcon": "public/icons/music256x256.png",
    "installerSidebar": "public/icons/music256x256.png",
    "uninstallerSidebar": "public/icons/music256x256.png",
    "uninstallDisplayName": "electron-music-${version}",
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "include": "script/installer.nsi",
    "script": "script/installer.nsi",
    "deleteAppDataOnUninstall": true,
    "runAfterFinish": false,
    "menuCategory": false,
    "perMachine": true,
    "language": "2052"
  },
  mac: {
    icon: 'public/icons/music256x256.png',
    category: 'Productivity',
    target: [
      {
        target: 'default',
        arch: [
          'arm64',
          'x64'
        ]
      }
    ]
  },
  linux: {
    icon: "public/icons/music256x256.png",
    target: [
      "AppImage",
      "tar.gz"
    ],
    "category": "Audio",
    artifactName: "${productName}-Linux-${version}.${ext}"
  }
}

参考: 私の音楽プレーヤープロジェクト

三 注意事項#

1 Linux でウィンドウを閉じる前に行うべきこと#

環境を確認します。例えば、kde ではウィンドウを隠すことができません。kde では、閉じるボタンをクリックすると、window オブジェクトは直接破棄されます。(曲線的な方法として、システムのデフォルトのタイトルバーを使用せず、自分で実装することで、この問題を回避できます。)

app.on('closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

2 Linux で NVIDIA グラフィックカードを使用している場合、アニメーションのあるページでエラーが発生する(未解決)#

エラー内容

libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null)
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。