# Electron講習 今日話すこと 1. Electronの軽い説明 2. 最小限構成の実装・解説 3. プロセス間通信について 4. アプリケーションの配布 ## Electronとは? HTMLやJSなどWeb系の知識でGUIアプリケーションを作れる. →Qtやxwindowなど別の知識をそこまで必要としない 必要な知識はWeb系のフロントと少しのNode.jsがわかればいける. Node.jsはほぼJSだし, Web系フロントができればNode.jsはそこまで敷居が高くない. マルチプラットフォームに対応している →同じプログラムでWindows, Linux, Macに対応できる!! ### Electronを使ったアプリケーションたち ![](https://i.imgur.com/JnDVGFW.png) ## Electronの構成図 ![](https://i.imgur.com/1Nn4XSf.jpg) **ElectronはNodeサーバーとブラウザエンジンのセット** →ブラウザエンジン実行し, ブラウザのGUIを描画しているだけ. →Web系で作ったフロントをほぼそのままで移行ができる(逆も然り) ## Electronの実装(HelloWorld) ### プロジェクトの作成 今回作成するアプリを管理するアプリのディレクトリを作成 `mkdir my-electron-app && cd my-electron-app` 管理用のファイルの生成 `npm init` この時色々質問されるがとりあえずエンターでもOK →この質問によって生成される`package.json`の内容が変わるだけ →後でpackage.jsonを書き換えればOK package.jsonのサンプル ```json= { "name": "helloworld-app", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "electron ." }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "electron": "^12.0.1" } } ``` **変更点** * mainを`main.js`に設定する 本来nodeの場合はmainは`index.js`だがelectronの場合はmain.jsにする習慣がある. * scriptsに`"start":"electron ."`を追加する これは`npm start`で実行されるコマンドとなっている. これを使うことでelectronを実行することができる **electronのインストール** `npm install --save-dev electron` →save-devが大切. →開発時のみelectronを使用するため, 必ずこの引数をつけましょう **main.jsの実装** ```javascript= const { app, BrowserWindow } = require('electron') function createWindow () { const win = new BrowserWindow({ width: 800, height: 600 }) win.loadFile('index.html') } app.whenReady().then(() => { createWindow() // macOS を除き、全ウインドウが閉じられたときに終了します。 app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) ``` **htmlの実装** ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello World!</title> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> </head> <body style="background: white;"> <h1>Hello World!</h1> <p> We are using node <script>document.write(process.versions.node)</script>, Chrome <script>document.write(process.versions.chrome)</script>, and Electron <script>document.write(process.versions.electron)</script>. </p> </body> </html> ``` ### 実装のポイント 基本は, * メインプロセスの動作を決めるmain.js * レンダラープロセスにて表示させるhtmlファイル という構成になる 基本的なappディレクトリ内の構成 ``` . ├── index.html ├── main.js ├── node_modules ├── package-lock.json └── package.json ``` ### 実行 `npm run start` ## Electronの実装(レンダラープロセスとメインプロセスの通信) Electronでレンダラープロセスとメインプロセスの通信は IPC(Inter-Process Communication)の技術を使用する. イメージとしては レンダラープロセスとメインプロセスでpub/subを組む感じ IPCを使用したレンダラーとメインプロセスの実装は具体的にいうと `レンダラープロセス ⇄ contextBridge ⇄ メインプロセス` という構成になる. 理由については後述する. **main.jsの実装** ```javascript= const { app, ipcMain, BrowserWindow } = require('electron') const path = require("path"); function createWindow () { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname + '/preload.js') //preloadするjs指定 } }) win.loadFile('index.html') } app.whenReady().then(() => { createWindow() // macOS を除き、全ウインドウが閉じられたときに終了します。 app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) ipcMain.handle("hello", (event) => { return "HelloWorld" }) ipcMain.on("send-message-sum", (event, data0, data1) => { const win = BrowserWindow.getFocusedWindow(); win.webContents.send("sum_result", (data0+data1)) }) ``` **preload.js** ```javascript= const { contextBridge, ipcRenderer} = require("electron"); contextBridge.exposeInMainWorld( "api", { //ipc send only send: (channel, ...data) => { ipcRenderer.send(channel, ...data); }, //ipc send and await return send_await: async (channel, ...data) => { const result = ipcRenderer.invoke(channel, ...data); return result }, //ipc on and callback func on: (channel, func) => { ipcRenderer.on(channel, (event, ...args) => func(...args)); } } ); ``` **index.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'"> <title>Hello World!</title> </head> <body> <h1>Hello IPC</h1> <h2>Hello Test</h2> <button type="button" id="send-message-hello">Send1</button> <div id="hello-result"></div> <h2>Sum Test</h2> <input type="number" id="sum-data0"> <input type="number" id="sum-data1"> <button type="button" id="send-message-sum">Send2</button> <div id="sum-result"></div> <script src="index.js"></script> </body> </html> ``` **index.js** ```javascript= document.getElementById("send-message-hello").addEventListener("click", async () => { const result = await window.api.send_await("hello"); console.log(result); document.getElementById("hello-result").textContent = result; }) document.getElementById("send-message-sum").addEventListener("click", () => { let data0 = parseFloat(document.getElementById("sum-data0").value); let data1 = parseFloat(document.getElementById("sum-data1").value); window.api.send('send-message-sum', data0, data1); }) window.api.on("sum_result", (sum_result)=> { console.log("sum result:", sum_result); document.getElementById("sum-result").textContent = sum_result; }) ``` **ディレクトリの構成** 今回はhtml内のjavascriptを実装するので分けて記述 →htmlにinlineでも書けることは書ける ``` . ├── index.html ├── index.js ├── main.js ├── node_modules ├── package-lock.json ├── package.json └── preload.js ``` ### 実装のポイント `ipcMain.handle`と`ipcRenderer.invoke` こいつらは基本的にセットで使う. このセットはpub/subよりはサーバークライアントモデルに近い運用が可能. `ipcRenderer.invoke`はメインプロセスにリクエストを送る `ipcMain.handle`はレンダラープロセスのリクエスを受け取って結果を返す. `ipcRenderer.send`はレンダラープロセスのpublish的な存在 `ipcRenderer.on`はレンダラープロセスのsubscribe的な存在 `ipcMain.on`はメインプロセスのsubscribe的な存在 `<window_obj>.webContents.send`はメインプロセスから特定のレンダラープロセスへpublishする. `<window_obj>`は `const win = BrowserWindow.getFocusedWindow();` でフォーカスしているレンダラープロセスのwindowを取得する. すべてのwindowへpublishしたい場合などは, `const win_list = BrowserWindow.getAllWindows();` で現在使用しているwindowを全てを取得してforなどでsendをループさせる. ### そもそもpreload.jsの役割ってなに? ipcRenderer.invoke等をレンダラープロセスで使用できるようにするもの electronに依存している部分をレンダラープロセスに渡す役割. `window.`で使用できるようになる. * *ちなみにネットで, メインプロセスにてwebPreferencesを ``` webPreferences: { nodeIntegration: true, contextIsolation: false } ``` に設定して, レンダラープロセスにて ```javascript const {ipcRenderer} = require('electron') ``` としてレンダラープロセスから直接ipcRendererを使用する!! みたいなものがまだあるが, これはbadパターンなので基本的に非推奨 実験とかなら許容できるが, 最終的にはやめましょう. なんで? これを使用するとxss攻撃なので簡単にipc通信を乗っ取れる webと違いelectronはローカル部分へのアクセスを許容しているので, 破壊的な操作も可能となってしまう. preload.jsを介して引数のフィルタリングなどをして安全に運用できるようにする. 他にも色々セキュリティ的なものはあるが, とりあえずここらへんはやっておきましょう. ## アプリケーションの配布 実行ファイルを生成して, Electronで実装したアプリケーションを簡単に他のpcにインストールすることが可能!! またクロスコンパイルにも対応しているのでmacがあれば, 基本的に全部のosに対応できる. win→macは自分のほうでは未検証 ### 準備 `npm install --save-dev electron-builder` package.jsonのscriptsに下記を追加 * mac : `"build-mac":"electron-builder --mac --x64"` * win : `"build-win":"electron-builder --win --x64"` * linux : `"build-linux": "electron-builder --linux --x64"` 例 ```json { "name": "ipc-communicate-safe-app", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "electron .", "build-mac": "electron-builder --mac --x64", "build-win": "electron-builder --win --x64", "build-linux": "electron-builder --linux --x64" }, "author": "", "license": "ISC", "devDependencies": { "electron": "^13.1.9", "electron-builder": "^22.11.7" } } ``` ### 実行ファイルの生成 mac : `npm run build-mac` win : `npm run build-win` linxu : `npm run build-linux` プロジェクトのディレクトリ内に`dist`という名前のディレクトリが生成される. その中に実行ファイルのexeやdmgが生成される. ### ビルドに失敗する クロスコンパイルをしているとos依存が高いものとかはビルドに失敗するときがある →そういった時はdockerを使うorその端末を用意する ## 参考 [公式ページ](https://www.electronjs.org/) https://github.com/KobayashiRui/Electron_practice/settings ###### tags: `Electron`