| ページ一覧 | ブログ | twitter |  書式 | 書式(表) |

MyMemoWiki

Electron

提供: MyMemoWiki
ナビゲーションに移動 検索に移動

| Node.js | JavaScript | TypeScript | npm | Flutter | React |

目次

Electron

Fiddle


API Document


Required

基本的なアプリの作成


  • Electronアプリケーションは本質的にNode.jsアプリケーション
  • Electronアプリケーションは、package.json から開始される

プロジェクトの作成とElectronのインストール

mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron
  • グローバルにインストール

npm

npm -g i electron

package.json


{
  "name": "electron_sample",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
  },
      :
}

mainスクリプトファイル(main.js)の作成


  • mainスクリプトは、Electronアプリケーションのエントリーポイント
  • Mainプロセスを開始し、Mainプロセスはアプリケーションのライフサイクルをコントロールする
const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
    const win = new BrowserWindow({
        width:400,
        height:300,
        webPreferences:{
            preload: path.join(__dirname, 'preload.js')
        }
    })
    win.loadFile('index.html')
}

app.whenReady().then(() => {
    createWindow()

    app.on('activate', () =>{
        if (BrowserWindow.getAllWindows().length == 0) {
            createWindow()
        }
    })
})

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

Webページ(index.html)の作成


  • index.html
  • アプリケーション初期化時に一度だけ表示されるページ
  • このページがレンダープロセスを表現する
<!DOCTYPE html>
<html>
<head>
    <meta carhset="UTF-8">
    <title>Electron Sample</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body style="background: white;">
    <h2>Version</h2>
    <div>
        We are using Node.js <span id="node-version"></span>
    </div>
    <div>
        Chromium <span id="chrome-version"></span>,
    </div>
    <div>
        Electron <span id="electron-version"></span>.
    </div>
</body>
</html>

プレロードスクリプト(preload.js)


  • Node.jsとWebページのブリッジ
  • Node.js全体を安全に公開するのではなく、特定のAPIや動作をWebページに公開することができる
  • 以下ではprocessオブジェクトからバージョン情報を読み取りページを更新する
window.addEventListener('DOMContentLoaded', () => {
    const replaceText = (selector, text) => {
      const element = document.getElementById(selector);
      if (element) {
          element.innerText = text;
      }
    }
  
    for (const type of ['chrome', 'node', 'electron']) {
      replaceText(`${type}-version`, process.versions[type])
    }
  })

.gitignore


起動


npm start

Electron sample start.png

Visual Studio Codeでのデバッグ


launch.json Node を Visual Studio Code でデバッグするときにグローバルにインストールしたモジュールが読み込まれない

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args" : ["."],
      "outputCapture": "std"
    }
  ]
}

Node ネイティブモジュール


Node.jsのネイティブモジュールはElectronでサポートされていますが、Electronは特定のNode.jsのバイナリとは異なるアプリケーション・バイナリ・インターフェース(ABI)を持っているため(OpenSSLの代わりにChromiumのBoringSSLを使用するなどの違いがあるため)、使用するネイティブモジュールはElectron用に再コンパイルする必要があります。そうしないと、アプリを実行しようとしたときに、以下のクラスのエラーが発生します。

Error: The module '/path/to/native/module.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION $XYZ. This version of Node.js requires
NODE_MODULE_VERSION $ABC. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).

モジュールのインストール方法


いくつかの方法がある

インストールし、Electron向けにリビルド

electron-rebuildパッケージを使ってElectron用にモジュールを再構築することができます。このモジュールは、Electronのバージョンを自動的に判断し、ヘッダーをダウンロードしてアプリ用のネイティブモジュールを再構築する手動ステップを処理することができます

npm install --save-dev electron-rebuild

# Every time you run "npm install", run this:
./node_modules/.bin/electron-rebuild

# If you have trouble on Windows, try:
.\node_modules\.bin\electron-rebuild.cmd

パッケージングと配布


electron-packager


Electron Packagerは、Electronベースのアプリケーションのソースコードを、リネームされたElectron実行ファイルおよびサポートファイルとともに、配布可能なフォルダにバンドルするコマンドラインツールおよびNode.jsライブラリです。

  • install
    • Globalインストールは非推奨
npm install --save-dev electron-packager
  • コマンドラインからの使用
npx electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
例(Mac)

Electron package.png

$ npx electron-packager . 
  1. プロジェクトディレクトリで $ npx electron-packager . を実行するだけ
  2. テンプレート:プロジェクト-darwin-x64 配下に出力
  3. Finderで確認
  4. 実行できる
Windows

Electron package win.png

  • Macと同様の手順でOK

Electron Forge


  • もっともシンプルで素早く配布するには、Electron Forgeを利用する
Electron ForgeをアプリケーションフォルダにImport
$ npm install --save-dev @electron-forge/cl
$ npx electron-forge import
✔ Checking your system
✔ Initializing Git Repository
✔ Writing modified package.json file
✔ Installing dependencies
✔ Writing modified package.json file
✔ Fixing .gitignore


We have ATTEMPTED to convert your app to be in a format that electron-forge understands.

Thanks for using "electron-forge"!!!
Mac 配布パッケージを作成

  • out フォルダに出力される
$ npm run make

> fx_sample@1.0.0 make
> electron-forge make

✔ Checking your system
✔ Resolving Forge Config
We need to package your application before we can make it
✔ Preparing to Package Application for arch: x64
✔ Preparing native dependencies
✔ Packaging Application
Making for the following targets: zip
✔ Making for target: zip - On platform: darwin - For arch: x64

Electron forge mac.png

Windows配布パッケージの作成

  • MacWindows用のパッケージ出力には、monoなどインストールが必要なようなので、Windows同様の手順でパッケージを作成。

Electron forge win.png

Ubuntu配布パッケージの作成

$ sudo apt install dpkg-dev
$ sudo apt install rpm
$ npm run make

Electron forge ubuntu.png

Electronの知識

プロセス


メインプロセスとレンダラープロセス


  • main.js がメインプロセスを担い、GUIは持たない
  • レンダーラープロセスは、Electronに内臓のWebブラウザを利用する
状況

2021/10現在、contextBridge利用が推奨

  • nodeIntegration: true -> これで、Renderer 側で、Node api が使えていたが、今は使えない、remoteも使えない
  • nodeIntegration: false, contextIsolation: false -> これで、Main と Rendererで同一のコンテキストとなるので、windowにipcReaderを登録して使える
  • nodeIntegration: false, contextIsolation: true -> contextBridge を使うことで、IPC通信ができる(contextIsolation true必須


remoteオブジェクト

【Electron】remote moduleがdeprecatedになっている背景

  • レンダラープロセスから、appやBrowserWindowなどのメインプロセス専用の機能を利用したい場合に用意されている
  • remoteは内部にまるでメインプロセスのモジュールが用意されているかのように振る舞う
const { remote } = require('electron');

IPC(プロセス間通信)


  • IPCについて
  • メインプロセスとレンダラープロセスで情報を授受する場合、IPCを利用する
  • ページAからページBを操作したい場合など、メッセージを ページA->メインプロセス->ページBと連携させる必要がある
ipcMain,ipcRenderer

Electron ipc.png

  • main.js Window生成
    • preload.js
    • contextIsolation: false
    let win = new BrowserWindow({
        width: 600,
        height: 400,
        webPreferences:{
            contextIsolation: false, // window object共有
            preload: path.join(__dirname, 'preload.js')
            // nodeIntegration: true,
            // enableRemoteModule: true
        }
    });
    // win.loadURL('https://service.typea.info/blogwiki');
    win.loadFile('index.html');
  • main.js 通信
const { ipcMain } = require('electron');
ipcMain.handle('invoke-test', (ev, msg) => {
    console.log("Message From Renderer:" + msg);
    return "Main response!"; 
});
  • preload.js
    • contextIsolation: false, // window object共有
const { ipcRenderer } = require('electron');
window.ipcRenderer = ipcRenderer;
  • index.html
    <script>
        ipcRenderer.invoke('invoke-test','sendmessage').then((data) => {
            console.log("Response from main:" + data);
        });
    </script>
contextBridge

contextBridgeを使えば、nodeIntegration: false,contextIsolation: trueでもIPC通信できる

Context Isolationは、プリロードスクリプトとElectronの内部ロジックが、webContentsでロードするWebサイトとは別のコンテキストで実行されるようにする機能です。これは、WebサイトがElectronの内部やプリロードスクリプトがアクセスできる強力なAPIにアクセスできないようにするためのセキュリティ上の重要な機能です。

つまり、プリロードスクリプトがアクセスできるウィンドウオブジェクトは、実際にはWebサイトがアクセスできるオブジェクトとは異なるものです。例えば、プリロードスクリプトでwindow.hello = 'wave'と設定し、コンテキストアイソレーションを有効にした場合、ウェブサイトがwindow.helloにアクセスしようとすると、window.helloは未定義となります。

コンテキスト分離はElectron 12からデフォルトで有効になっており、すべてのアプリケーションで推奨されるセキュリティ設定です。

  • main.js Window生成
    • contextBridgeを使う場合、contextIsolation:true とする必要あり
    let win = new BrowserWindow({
        width: 600,
        height: 400,
        webPreferences:{
            contextIsolation: true, // false -> window object共有、contextBridge利用時はtrue
            preload: path.join(__dirname, 'preload.js'),
            // enableRemoteModule: false
            // nodeIntegration: true,
        }
    });
    // win.loadURL('https://service.typea.info/blogwiki');
    win.loadFile('index.html');
  • main.js 通信
const { ipcMain } = require('electron');
ipcMain.handle('invoke-test', (ev, msg) => {
    console.log("Message From Renderer:" + msg);
    return "Main response!"; 
});
  • preload.js
const electron = require('electron');
const { ipcRenderer, contextBridge } = electron;

contextBridge.exposeInMainWorld(
    "api",
    {
        openWinWitMessage: (message) => {
            ipcRenderer.invoke('invoke-test', message).then((data) => {
                console.log("Response from main:" + data);
            });
        }
    }
);
  • index.tml
        function openWinContextBridge() {
            window.api.openWinWitMessage("use contextBridge!!");
        }

Electron contextbridge.png

contextBridge(main から rendelerの呼び出し)

  • main.js
  • 1秒ごとに時間を送信
    let win = new BrowserWindow({
        width: 600,
        height: 400,
        webPreferences:{
            preload: path.join(__dirname, 'preload.js'),
        }
    });
    win.loadFile('index.html');

    setInterval(() => {
        var now = new Date().toISOString();
        console.log(now);
        win.webContents.send('timer', now);
    }, 1000);
  • preload.js
const electron = require('electron');
const { ipcRenderer, contextBridge /*remote*/ } = electron;

contextBridge.exposeInMainWorld(
    "api",
    {
        on: (channel, callback) => {
            ipcRenderer.on(channel, (event, argv)=>callback(event, argv))
        }
    }
);
  • index.html
        window.api.on('timer', (event, time)=>{
            document.getElementById('timer').innerText = time;
        });

Electron contextbridge frommain.png

オブジェクト

app


  • アプリケーション本体
  • 起動/終了、Windowオープン/クローズなどの管理

BrowserWindow


  • Electronアプリで表示されるウィンドウオブジェクト
  • HTMLファイルを読み込むことでウィンドウを表示する

WebContents


  • BrowserWindowに含まれ、Webコンテンツの状態管理
  • Webコンテンツに関するイベント

BrowserView


  • BrowserWindowでloadFile、、loadURLを使って表示するコンテンツに、さらにWebコンテンツを埋め込む
  • BrowserWindow内部に小さな内部Windowのようなものを追加し、別コンテンツを表示できる
    const view = new BrowserView();
    view.webContents .loadURL('https://service.typea.info/blogwiki');
    win.setBrowserView(view);
    view.setBounds({
        x : 100,
        y : 150,
        width : 300,
        height : 150
    });

Electron browser view.png

Menu


  • Menu,MenuItem
  • clickイベント
    let menu = new Menu();

    let menuFile = new MenuItem({
        label: 'ファイル',
        submenu: [
            { label: '新規2', click: () => { console.log('新規2'); } },
            new MenuItem({ label: '開く' }),
            new MenuItem({ label: '終了' }),
        ]
    });
    menu.append(menuFile);

    Menu.setApplicationMenu(menu);

Electron menu.png

テンプレートからメニューを作成する


  • セパレータは、type: 'separator'
    let menuFileTemplate = [
        {
            label: 'ファイル',
            submenu: [
                { label: '新規2', click: () => { console.log('新規2'); } },
                { label: '開く2' },
                { type: 'separator' },
                { label: '終了2' },
            ]
        }
    ];
    menu = Menu.buildFromTemplate(menuFileTemplate);
    Menu.setApplicationMenu(menu);

role


  • roleを指定するとロールの機能が組み込まれる
    • about
    • undo
    • redo
    • cut
    • copy
    • paste
    • pasteAndMatchStyle
    • selectAll
    • delete
    • minimize
    • close
    • quit
    • reload
    • forceReload
    • toggleDevTools
    • togglefullscreen
    • resetZoom
    • zoomIn
    • zoomOut
    • fileMenu
    • editMenu
    • viewMenu
    • windowMenu
        {
            label: '編集',
            submenu: [
                { label: '切り取り', role: 'cut' },
                { label: 'コピー', role: 'copy' },
                { label: '貼り付け', role: 'paste' },
            ]
        }

Electron menu role.png

コンテキストメニュー


  • contextBridgeを利用する

Electron contextmenu.png

  • index.html
        function openContextMenu(e) {
            e.preventDefault();
            window.api.openContextMenu("hoge");
        }
        window.addEventListener('contextmenu', openContextMenu, false);
  • preload.js
contextBridge.exposeInMainWorld(
    "api",
    {
       openContextMenu: (type) => {
            return ipcRenderer.invoke('open-context-menu', type);
        }
    }
);
  • main.js
const { ipcMain, BrowserWindow, Menu, dialog } = require('electron');

ipcMain.handle('open-context-menu', (ev, msg) => {
    var win = BrowserWindow.getFocusedWindow();
    let contextmenuTemplate = [
        {
            label: msg, click() {
                dialog.showMessageBox(win, {message : msg} );
            }
        },
        { type: 'separator' },
        { label: '切り取り', role: 'cut' },
        { label: 'コピー', role: 'copy' },
        { label: '貼り付け', role: 'paste' },
    ];
    const contextMenu = Menu.buildFromTemplate(contextmenuTemplate);
    contextMenu.popup({window : win});
});

Dialog


contextBridge を使用してファイル選択ダイアログを表示する

const { ipcMain, dialog } = require('electron');
ipcMain.handle('open-file-dialog', async (ev, msg) => {
    var win = BrowserWindow.getFocusedWindow();
    var result =  await dialog.showOpenDialog(win, { properties: ['openFile', 'multiSelections'] });
    if (result.canceld) {
        return [];
    }
    return result.filePaths;
});
  • preload.js
const electron = require('electron');
const { ipcRenderer, contextBridge } = electron;

contextBridge.exposeInMainWorld(
    "api",
    {
        openFileDialog: (message) => {
            return ipcRenderer.invoke('open-file-dialog', message);
        }
    }
);
  • index.html
        async function  openWinFileDialog() {
            var filePaths = await window.api.openFileDialog("");
            alert(filePaths[0]);
        }

構成

main.js


const { app, BrowserWindow} = require('electron');

function createWindow() {
    let win = new BrowserWindow({
        width: 400,
        height: 200,
        webPreferences:{
            contextIsolation: false, // window object共有
            preload: path.join(__dirname, 'preload.js')
            // nodeIntegration: true,
            // enableRemoteModule: true
        }
    });
    win.loadFile('index.html');
}

app.whenReady().then(createWindow);

オブジェクトの分割代入


const { app, BrowserWindow} = require('electron');

Preloadスクリプト


プリロードスクリプトには、ウェブコンテンツの読み込み開始前にレンダラープロセスで実行されるコードが含まれています。これらのスクリプトはレンダラーのコンテキスト内で実行されますが、Node.jsのAPIにアクセスできるため、より多くの権限が与えられています。

Node.js機能の統合


  • trueでNode.jsの機能(通常のWebで使用できないrequireなど)を利用できるようになる
nodeIntegration: true

remoteモジュールの有効化


  • remoteモジュールの有効化
enableRemoteModule: true

リモートコンテンツを読み込むレンダラー(BrowserWindow、BrowserView、<webview>)では、Node.jsの統合を有効にしないことが最も重要です。この目的は、リモートコンテンツに与える権限を制限することで、攻撃者がウェブサイト上でJavaScriptを実行できるようになった場合に、ユーザーに危害を加えることを劇的に難しくすることです。

この後、特定のホストに対して追加の権限を付与することができます。例えば、https://example.com/ に向けてBrowserWindowを開いている場合、そのWebサイトが必要とする能力を正確に与えることができますが、それ以上はできません。

Webページをロード


  • loadURLとすることで、外部ページをロードできる
   // win.loadFile('index.html');
   win.loadURL('https://service.typea.info/blogwiki');
 

Electron web app.png

モーダルダイアログ


function createWindow() {
    let win = new BrowserWindow({
        width: 600,
        height: 400,
        webPreference: {
            nodeIntegration: true,
            enableRemoteModule
        }
    });
    // win.loadURL('https://service.typea.info/blogwiki');
    win.loadFile('index.html');

    let child = new BrowserWindow({
        width: 400,
        height: 200,
        parent: win,
        frame: false,
        modal: true,
        transparent: true,
        opacity: 0.5
    });
    child.loadFile('dialog.html');
}

Electron modal dialog.png

デベロッパーツールを開く


win.webContents.openDevTools();

Electron devtools.png

appオブジェクトのイベント

起動処理の完了

will-finish-launching

初期化処理完了

ready

BrowserWindowの生成

browser-window-created

Webコンテンツの生成

web-contents-created

全てのWindowが閉じられた

window-all-closed

全てのWindowを閉じる前

before-quit

終了する前

will-quit

終了時

quit

BrowserWindowのイベント

Windowの表示

show

Windowの非表示

hide

Window表示準備完了

ready-to-show

Windowを閉じる

close

Windowが閉じられた

closed

その他

  • focus
  • blur
  • maximize
  • unmaximize
  • minimize
  • restore
  • will-resize
  • resize
  • will-move
  • move
  • enter-full-screen
  • leave-full-screen
  • enter-html-full-screen
  • leave-html-full-screen
  • always-on-top-changed

BrowseWindow操作

  • destory
  • close()
  • focus()
  • blur()
  • isFocuced()
  • isDestoryed()
  • show()
  • showInactive()
  • hide()
  • isVisible()
  • isModal()
  • maximize()
  • unmaximize()
  • isMaximized()
  • minimize()
  • restore
  • isMinimized()
  • setFullScreen()
  • isFullScreen()
  • isNormal()
  • SetBounds()
  • GetBounds()
  • SetContentBounds()
  • GetContentsBound()
  • SetSize()
  • GetSize()
  • SetContaentSize()
  • GetContentSize()
  • SetMinimumSize()
  • GetMinimumSize
  • SetMaximumSize()
  • GetMaximumSize()
  • SetPosition()
  • GetPosition()
  • moveTop()
  • center()
  • settitle()
  • getTitle()

WebContentsのイベント

コンテンツロード完了

did-finish-load

フレームのコンテンツロード

did-frame-finish-load

コンテンツ読み込み開始

did-start-loading

コンテンツ読み込み中止

did-stop-loading

DOM生成完了

dom-ready

新しいWindow作成

new-window

URLアクセス時

will-navigate

URLアクセス開始

did-start-navigation

その他

  • will-redirect
  • did-redirect-navigation
  • did-navigate
  • will-prevent-unload
  • destroyed
  • enter-html-full-screen
  • leave-html-full-screen
  • zoom-changed
  • devtools-opend
  • devtools-closed
  • devtools-focused
  • console-message

Tips

ファイルを開く


  • コンテキストメニューからファイルを開くダイアログで選択したファイルを画面に表示
  • main.js
ipcMain.handle('open-context-menu', (ev, msg) => {
    var win = BrowserWindow.getFocusedWindow();
    let contextmenuTemplate = [
        {
            label: 'ファイルを開く', click() {
                openFile(win);
            }
        }
    ];
    const contextMenu = Menu.buildFromTemplate(contextmenuTemplate);
    contextMenu.popup({window : win});
});

async function openFile(win) {
    var result =  await dialog.showOpenDialogSync(
        win, { 
            properties: ['openFile'],
            filters: [
                {name:'text', extensions: ['txt'] },
                {name:'all', extensions: ['*'] },
            ]
        });
    if (result.canceld) {
        return;
    }
    var filePath = result[0];
    var content = fs.readFileSync(filePath).toString();
    win.webContents.send('open-file', content);
}
  • preload.js
contextBridge.exposeInMainWorld(
    "api",
    {
        openContextMenu: (type) => {
            return ipcRenderer.invoke('open-context-menu', type);
        },
        on: (channel, callback) => {
            ipcRenderer.on(channel, (event, argv)=>callback(event, argv))
        }
    }
);
  • index.html
        window.addEventListener('contextmenu', openContextMenu, false);

        window.api.on('open-file', (event, content)=>{
            document.getElementById('open_file').value = content;
        });

ファイルを保存


  • main.js
const { ipcMain, app, BrowserWindow,  Menu, MenuItem, dialog } = require('electron');
const fs = require('fs');

ipcMain.handle('open-context-menu', (ev, msg) => {
    var win = BrowserWindow.getFocusedWindow();
    let contextmenuTemplate = [
        {
            label: 'ファイルを保存', click() {
                saveFile(win);
            }
        },
    ];

async function saveFile(win) {
    var result = await dialog.showSaveDialogSync(
        win, {
            properties: ['']
        });
    if (result.canceld) {
        return;
    }
    var filePath = result;
    var data = await win.webContents.executeJavaScript('window.document.getElementById("open_file").value');
    fs.writeFileSync(filePath, data);
}
  • index.html
        <textarea id="open_file" rows="10" cols="80"></textarea>
               :
        function openContextMenu(e) {
            e.preventDefault();
            window.api.openContextMenu("hoge");
        }
        window.addEventListener('contextmenu', openContextMenu, false);

httpを用いてデータを取得


Electron http get.png

  • Node の https パッケージでは使い勝手が割るので、superagent を導入
$ npm install --save superagent
  • index.html
        <textarea id="open_file" rows="10" cols="80"></textarea>
        <input type="text"  id="getHttpDataUrl" value="https://www.typea.info/blog/index.php/feed/" />
        <input type="button" id="btnGetHttpData" value="get http data" />
              :
        async function getHttpData() {
            var data = await window.api.getHttpData(document.getElementById('getHttpDataUrl').value);
            document.getElementById("open_file").value = data;
        }
        document.getElementById("btnGetHttpData").addEventListener('click', getHttpData);   
  • preload.js
contextBridge.exposeInMainWorld(
    "api",
    {
        getHttpData: (url) => {
            return ipcRenderer.invoke('get-http-data', url);
        },
    }
);
  • main.js
ipcMain.handle('get-http-data', async (ev, url) =>{
    // https://www.typea.info/blog/index.php/2017/08/19/react_react_router_redux-saga_ajax/
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // 開発用証明書エラー無視
    var response = await request.get(url).query().buffer();
    console.log(response);
    return response.res.text;
});

sqlite


Electron sqlite3.png

$ npm install --save-dev electron-rebuild
$ npm install --save sqlite3
$ ./node_modules/.bin/electron-rebuild -f -w sqlite3
  • main.js
const sqlite3 = require('sqlite3');
    :
    var db = new sqlite3.Database(filePath);
    db.serialize(() => {
        db.run("create table if not exists test(id int primary key, value text)");
    });
    db.close();