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

MyMemoWiki

差分

ナビゲーションに移動 検索に移動
19,687 バイト追加 、 2021年10月11日 (月) 12:15
編集の要約なし
| [[Node.js]] | [[JavaScript]] | [[TypeScript]] | [[npm]] | [[Flutter]] | [[React]] |
==Electron==
{{amazon|B07QPQ24BN}}
<pre>
npm -g i electron
</pre>
 
====package.json====
----
<pre>
{
"name": "electron_sample",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
},
:
}
</pre>
})
</pre>
====package.json====
----
<pre>
{
"name": "electron_sample",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
},
:
}
</pre>
 
====.gitignore====
----
]
}
</pre>
 
===Node ネイティブモジュール===
----
*https://www.electronjs.org/docs/tutorial/using-native-node-modules
<blockquote>
Node.jsのネイティブモジュールはElectronでサポートされていますが、Electronは特定のNode.jsのバイナリとは異なるアプリケーション・バイナリ・インターフェース(ABI)を持っているため(OpenSSLの代わりにChromiumのBoringSSLを使用するなどの違いがあるため)、使用するネイティブモジュールはElectron用に再コンパイルする必要があります。そうしないと、アプリを実行しようとしたときに、以下のクラスのエラーが発生します。
</blockquote>
<pre>
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`).
</pre>
====モジュールのインストール方法====
----
いくつかの方法がある
=====インストールし、Electron向けにリビルド=====
<blockquote>
electron-rebuildパッケージを使ってElectron用にモジュールを再構築することができます。このモジュールは、Electronのバージョンを自動的に判断し、ヘッダーをダウンロードしてアプリ用のネイティブモジュールを再構築する手動ステップを処理することができます
</blockquote>
<pre>
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
</pre>
===パッケージングと配布===
----
====[https://www.npmjs.com/package/electron-packager electron-packager]====
----
*https://webbibouroku.com/Blog/Article/electron#outline__3
<blockquote>
Electron Packagerは、Electronベースのアプリケーションのソースコードを、リネームされたElectron実行ファイルおよびサポートファイルとともに、配布可能なフォルダにバンドルするコマンドラインツールおよびNode.jsライブラリです。
</blockquote>
*install
**Globalインストールは非推奨
<pre>
npm install --save-dev electron-packager
</pre>
*コマンドラインからの使用
<pre>
npx electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
</pre>
=====例([[Mac]])=====
[[File:electron_package.png|500px]]
<pre>
$ npx electron-packager .
</pre>
# プロジェクトディレクトリで $ npx electron-packager . を実行するだけ
# {{プロジェクト}}-darwin-x64 配下に出力
# Finderで確認
# 実行できる
=====例[[Windows]]=====
[[File:electron_package_win.png|500px]]
*[[Mac]]と同様の手順でOK
 
====[https://www.electronforge.io/ Electron Forge]====
----
*https://blog.ikappio.com/electron-forge-make-for-windows-from-macos/
*ビルド環境と同じパッケージをデフォルトで生成するようだ。
 =====[[Windows]]配布パッケージの作成=====
----
*[[Mac]] で [[Windows]]用のパッケージ出力には、monoなどインストールが必要なようなので、[[Windows]]同様の手順でパッケージを作成。
[[File:electron_forge_win.png|400px]]
 =====[[Ubuntu]]配布パッケージの作成=====
----
<pre>
</pre>
[[File:electron_forge_ubuntu.png|400px]]
 
==Electronの知識==
===プロセス===
*main.js がメインプロセスを担い、GUIは持たない
*レンダーラープロセスは、Electronに内臓のWebブラウザを利用する
 =====状況=====-----2021/10現在、contextBridge利用が推奨*nodeIntegration: true -> これで、Renderer 側で、Node api が使えていたが、今は使えない、remoteも使えない*Electronの機能を利用する場合、、electronnodeIntegration: false, contextIsolation: false -> これで、Main と Rendererで同一のコンテキストとなるので、windowにipcReaderを登録して使える* nodeIntegration: false, contextIsolation: true -> contextBridge を使うことで、IPC通信ができる(contextIsolation true必須  =====remoteオブジェクト=====----<blockquote>[https://simple-minds-think-alike.moritamorie.remote経由com/entry/disable-electron-remote-module 【Electron】remote moduleがdeprecatedになっている背景]</blockquote>*レンダラープロセスから、appやBrowserWindowなどのメインプロセス専用の機能を利用したい場合に用意されている*remoteは内部にまるでメインプロセスのモジュールが用意されているかのように振る舞う<pre>const { remote } = require('electron');</pre>
====IPC(プロセス間通信)====
----
*[https://qiita.com/hibara/items/c59fb6924610fc22a9db IPCについて]
*メインプロセスとレンダラープロセスで情報を授受する場合、IPCを利用する
*ページAからページBを操作したい場合など、メッセージを ページA->メインプロセス->ページBと連携させる必要がある
 ==構成===ipcMain,ipcRenderer==mainn.js===
----
[[File:electron_ipc.png|400px]]
*main.js Window生成
**preload.js
** contextIsolation: false
<pre>
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');</pre>*main.js 通信<pre>const { ipcMain } = require('electron');ipcMain.handle('invoke-test', (ev, msg) => { console.log("Message From Renderer:" + msg); return "Main response!"; });</pre>*preload.js** contextIsolation: false, // window object共有 <pre>const { appipcRenderer } = require('electron');window.ipcRenderer = ipcRenderer;</pre>*index.html<pre> <script> ipcRenderer.invoke('invoke-test', 'sendmessage').then((data) => { console.log("Response from main:" + data); }); </script></pre>=====contextBridge=====----- *[https://qiita.com/pochman/items/64b34e9827866664d436 contextBridgeについて]*[https://blog.katsubemakito.net/nodejs/electron/ipc-for-contextbridge contextBridgeでのIPC]contextBridgeを使えば、nodeIntegration: false,contextIsolation: trueでもIPC通信できる*[https://www.electronjs.org/docs/tutorial/context-isolation contextIsolation]<blockquote>Context Isolationは、プリロードスクリプトとElectronの内部ロジックが、webContentsでロードするWebサイトとは別のコンテキストで実行されるようにする機能です。これは、WebサイトがElectronの内部やプリロードスクリプトがアクセスできる強力なAPIにアクセスできないようにするためのセキュリティ上の重要な機能です。 つまり、プリロードスクリプトがアクセスできるウィンドウオブジェクトは、実際にはWebサイトがアクセスできるオブジェクトとは異なるものです。例えば、プリロードスクリプトでwindow.hello = 'wave'と設定し、コンテキストアイソレーションを有効にした場合、ウェブサイトがwindow.helloにアクセスしようとすると、window.helloは未定義となります。 コンテキスト分離はElectron 12からデフォルトで有効になっており、すべてのアプリケーションで推奨されるセキュリティ設定です。</blockquote>*main.js Window生成**contextBridgeを使う場合、contextIsolation:true とする必要あり<pre> 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');</pre>*main.js 通信<pre>const { ipcMain } = require('electron');ipcMain.handle('invoke-test', (ev, msg) => { console.log("Message From Renderer:" + msg); return "Main response!"; } );</pre>*preload.js<pre>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); }); } });</pre>*index.tml<pre> function openWinContextBridge() { window.api.openWinWitMessage("use contextBridge!!"); }</pre>[[file:electron_contextbridge.png|400px]]
function createWindow=====contextBridge(main から rendelerの呼び出し) {=====-----*main.js*1秒ごとに時間を送信<pre>
let win = new BrowserWindow({
width: 400600, height: 200400, webPreferencewebPreferences: { nodeIntegrationpreload: truepath.join(__dirname, 'preload.js'),
}
});
win.loadFile('index.html');
}
app setInterval(() => { var now = new Date().toISOString(); console.log(now); win.webContents.send('timer', now); }, 1000);</pre>*preload.js<pre>const electron = require('electron');const { ipcRenderer, contextBridge /*remote*/ } = electron; contextBridge.exposeInMainWorld( "api", { on: (channel, callback) => { ipcRenderer.on(channel, (event, argv)=>callback(event, argv)) } });</pre>*index.whenReadyhtml<pre> window.api.on('timer', (event, time)=>{ document.thengetElementById(createWindow'timer').innerText = time; });
</pre>
====オブジェクトの分割代入==== const { app, BrowserWindow} = require('electron');====[[Nodefile:electron_contextbridge_frommain.jspng|400px]]機能の統合====*trueで[[Node.js]]の機能を利用できるようになる nodeIntegration: true
==オブジェクト==
*BrowserWindowに含まれ、Webコンテンツの状態管理
*Webコンテンツに関するイベント
===BrowserView===
----
*BrowserWindowでloadFile、、loadURLを使って表示するコンテンツに、さらにWebコンテンツを埋め込む
*BrowserWindow内部に小さな内部Windowのようなものを追加し、別コンテンツを表示できる
<pre>
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
});
</pre>
 
[[File:electron_browser_view.png|400px]]
===Menu===
----
*Menu,MenuItem
*clickイベント
<pre>
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);
</pre>
[[File:electron_menu.png|300px]]
====テンプレートからメニューを作成する====
----
* セパレータは、type: 'separator'
<pre>
let menuFileTemplate = [
{
label: 'ファイル',
submenu: [
{ label: '新規2', click: () => { console.log('新規2'); } },
{ label: '開く2' },
{ type: 'separator' },
{ label: '終了2' },
]
}
];
menu = Menu.buildFromTemplate(menuFileTemplate);
Menu.setApplicationMenu(menu);
</pre>
====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
<pre>
{
label: '編集',
submenu: [
{ label: '切り取り', role: 'cut' },
{ label: 'コピー', role: 'copy' },
{ label: '貼り付け', role: 'paste' },
]
}
</pre>
[[File:electron_menu_role.png|300px]]
 
====コンテキストメニュー====
----
*contextBridgeを利用する
[[File:electron_contextmenu.png|300px]]
 
*index.html
<pre>
function openContextMenu(e) {
e.preventDefault();
window.api.openContextMenu("hoge");
}
window.addEventListener('contextmenu', openContextMenu, false);
</pre>
*preload.js
<pre>
contextBridge.exposeInMainWorld(
"api",
{
openContextMenu: (type) => {
return ipcRenderer.invoke('open-context-menu', type);
}
}
);
</pre>
*main.js
<pre>
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});
});
</pre>
 
===Dialog===
----
*https://www.electronjs.org/docs/api/dialog
**showOpenDialogSync
**showOpenDialog
**showSaveDialogSync
**showSaveDialog
**showMessageBoxSync
**showMessageBox
**showErrorBox
**showCertificateTrustDialog
 
contextBridge を使用してファイル選択ダイアログを表示する
*[https://www.electronjs.org/docs/api/browser-window BrowserWindow.getFocusedWindow()] で Windowハンドルを得る
*main.js
<pre>
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;
});
</pre>
*preload.js
<pre>
const electron = require('electron');
const { ipcRenderer, contextBridge } = electron;
 
contextBridge.exposeInMainWorld(
"api",
{
openFileDialog: (message) => {
return ipcRenderer.invoke('open-file-dialog', message);
}
}
);
</pre>
*index.html
<pre>
async function openWinFileDialog() {
var filePaths = await window.api.openFileDialog("");
alert(filePaths[0]);
}
</pre>
 
==構成==
===main.js===
----
<pre>
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);</pre>====オブジェクトの分割代入====---- const { app, BrowserWindow} = require('electron');====Preloadスクリプト====----*https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts '''プリロードスクリプトには、ウェブコンテンツの読み込み開始前にレンダラープロセスで実行されるコードが含まれています。これらのスクリプトはレンダラーのコンテキスト内で実行されますが、Node.jsのAPIにアクセスできるため、より多くの権限が与えられています。''' ====[[Node.js]]機能の統合====----*trueで[[Node.js]]の機能(通常のWebで使用できないrequireなど)を利用できるようになる nodeIntegration: true ====remoteモジュールの有効化====----*remoteモジュールの有効化 enableRemoteModule: true *https://www.electronjs.org/docs/tutorial/security**Do not enable Node.js Integration for Remote Content '''リモートコンテンツを読み込むレンダラー(BrowserWindow、BrowserView、<webview>)では、Node.jsの統合を有効にしないことが最も重要です。この目的は、リモートコンテンツに与える権限を制限することで、攻撃者がウェブサイト上でJavaScriptを実行できるようになった場合に、ユーザーに危害を加えることを劇的に難しくすることです。 この後、特定のホストに対して追加の権限を付与することができます。例えば、https://example.com/ に向けてBrowserWindowを開いている場合、そのWebサイトが必要とする能力を正確に与えることができますが、それ以上はできません。''' ====Webページをロード====
----
*loadURLとすることで、外部ページをロードできる
*[https://service.typea.info/blogwiki Webページ]をElectronアプリ化
[[File:electron_web_app.png|500px]]
 ====モーダルダイアログ====
----
<pre>
height: 400,
webPreference: {
nodeIntegration: true, enableRemoteModule
}
});
[[File:electron_modal_dialog.png|400px]]
====デベロッパーツールを開く====
----
win.webContents.openDevTools();
*devtools-focused
*console-message
==Tips==
===ファイルを開く===
----
*コンテキストメニューからファイルを開くダイアログで選択したファイルを画面に表示
*main.js
<pre>
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);
}
</pre>
*preload.js
<pre>
contextBridge.exposeInMainWorld(
"api",
{
openContextMenu: (type) => {
return ipcRenderer.invoke('open-context-menu', type);
},
on: (channel, callback) => {
ipcRenderer.on(channel, (event, argv)=>callback(event, argv))
}
}
);
</pre>
*index.html
<pre>
window.addEventListener('contextmenu', openContextMenu, false);
 
window.api.on('open-file', (event, content)=>{
document.getElementById('open_file').value = content;
});
</pre>
 
===ファイルを保存===
----
*main.js
<pre>
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);
}
</pre>
*index.html
<pre>
<textarea id="open_file" rows="10" cols="80"></textarea>
:
function openContextMenu(e) {
e.preventDefault();
window.api.openContextMenu("hoge");
}
window.addEventListener('contextmenu', openContextMenu, false);
</pre>
 
===httpを用いてデータを取得===
----
[[File:electron_http_get.png|400px]]
*Node の https パッケージでは使い勝手が割るので、superagent を導入
<pre>
$ npm install --save superagent
</pre>
*index.html
<pre>
<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);
</pre>
*preload.js
<pre>
contextBridge.exposeInMainWorld(
"api",
{
getHttpData: (url) => {
return ipcRenderer.invoke('get-http-data', url);
},
}
);
</pre>
*main.js
<pre>
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;
});
</pre>
===sqlite===
----
*https://www.virment.com/use-sqlite3-electron-vue-js/
[[File:electron_sqlite3.png|400px]]
<pre>
$ npm install --save-dev electron-rebuild
$ npm install --save sqlite3
$ ./node_modules/.bin/electron-rebuild -f -w sqlite3
</pre>
 
*main.js
<pre>
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();
</pre>

案内メニュー