Skip to content

Commit 0c629b7

Browse files
committed
electron
1 parent 7b69ec3 commit 0c629b7

File tree

2 files changed

+283
-0
lines changed

2 files changed

+283
-0
lines changed

electron.rst

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
Electronアプリケーションの作成
2+
========================================
3+
4+
マルチプラットフォームなデスクトップアプリケーションを簡単に作る方法として近年人気なのがGitHubの開発したElectronを使った開発です。ChromiumとNode.jsが一体となった仕組みになっています。UIはブラウザで「レンダープロセス」が担い、そのUIの起動やローカルファイルへのアクセスなどを行うのが「メインプロセス」です。
5+
6+
レンダープロセスとメインプロセスはなるべく疎結合に作ります。プログラムの量はおそらくレンダープロセスが95%ぐらいの分量になるでしょう。SPAのウェブフロントエンドとウェブサーバーを作る感覚よりも、さらにフロントに荷重が寄った構造になるでしょう。
7+
8+
Electronのランタイムと、ビルドしたJavaScriptをまとめて、インストーラまで作成してくれるのがElectron-Buildです。これを使ってアプリケーションの開発を行っていきます。Vueの場合はVue CLI用のプラグイン\ [#]_\ があります。
9+
10+
* https://www.electron.build/
11+
12+
.. [#] https://nklayman.github.io/vue-cli-plugin-electron-builder/
13+
14+
React+Electronの環境構築の方法
15+
-----------------------------------
16+
17+
Electronの開発は2つ作戦が考えられます。一つが、ウェブのフロントエンドとして、そちらのエコシステムを利用して開発します。もう一つが、普段の開発からElectronをランタイムとして開発する方法です。どちらか慣れている方で良いでしょう。
18+
19+
後者については次のQiitaのエントリーが詳しいです。
20+
21+
* https://qiita.com/yhirose/items/22b0621f0d36d983d8b0
22+
* https://github.com/yhirose/react-typescript-electron-sample-with-create-react-app-and-electron-builder
23+
24+
本書では、前者の方法について紹介します。まず、プロジェクトを作成します。今回は2つのエントリーポイントのビルドが必要なため、これに対応しやすいParcelを利用します。
25+
26+
.. code-block:: bash
27+
28+
# プロジェクトフォルダ作成
29+
$ mkdir electronsample
30+
$ cd electronsample
31+
$ npm init -y
32+
33+
# 必要なツールをインストール
34+
$ npm install --save-dev parcel@next typescript @parcel/[email protected]
35+
$ npm install --save-dev react @types/react react-dom @types/react-dom
36+
$ npm install --save-dev electron npm-run-all
37+
38+
# tsconfig作成
39+
$ npx tsc --init
40+
41+
tsconfig.jsonは、いつものようにtarget/module/jsxあたりを修正しておきます。
42+
43+
.. code-block:: json
44+
:caption: tsconfig.json
45+
46+
{
47+
"compilerOptions": {
48+
"target": "es2020",
49+
"module": "es2015",
50+
"jsx": "react",
51+
"strict": true,
52+
"moduleResolution": "node",
53+
"esModuleInterop": true,
54+
"skipLibCheck": true,
55+
"forceConsistentCasingInFileNames": true
56+
}
57+
}
58+
59+
もう一つ、メインプロセス用のtsconfigも作ります。こちらはNode.js用に近い形式で出力が必要なため、commonjs形式のモジュールに設定しています。
60+
61+
.. code-block:: json
62+
:caption: tsconfig.main.json
63+
64+
{
65+
"extends": "./tsconfig.json",
66+
"compilerOptions": {
67+
"outDir": "dist",
68+
"module": "commonjs",
69+
"sourceMap": true,
70+
},
71+
"include": [
72+
"src/main/*"
73+
]
74+
}
75+
76+
次にファイルを3つ作ります。
77+
78+
.. code-block:: html
79+
:caption: src/render/index.html
80+
81+
<!DOCTYPE html>
82+
<head>
83+
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
84+
</head>
85+
<body>
86+
<div id="app"></div><script src="index.tsx"></script>
87+
</body>
88+
89+
.. code-block:: ts
90+
:caption: src/render/index.tsx
91+
92+
import React from "react";
93+
import { render } from "react-dom";
94+
95+
const App = () => <h1>Hello!</h1>;
96+
97+
render(<App />, document.getElementById("app"));
98+
99+
.. code-block:: ts
100+
:caption: src/main/main.ts
101+
102+
import { app, BrowserWindow } from 'electron';
103+
104+
let win: BrowserWindow | null = null;
105+
106+
function createWindow() {
107+
win = new BrowserWindow({ width: 800, height: 600 })
108+
win.loadURL(`file://${__dirname}/index.html`);
109+
win.on('closed', () => win = null);
110+
}
111+
112+
app.on('ready', createWindow);
113+
114+
app.on('window-all-closed', () => {
115+
if (process.platform !== 'darwin') {
116+
app.quit();
117+
}
118+
});
119+
120+
app.on('activate', () => {
121+
if (win === null) {
122+
createWindow();
123+
}
124+
});
125+
126+
package.jsonのスクリプトも追加しておきましょう。レンダープロセス部分はPercelを使い、メインプロセスにはTypeScriptのtscコマンドをダイレクトで使っています。tscはバンドルをせずに、ソースファイルに対して1:1で変換した結果を出力します。メインプロセスは@vercel/nccを使っても良いと思いますが、Electronではレンダープロセス起動時の初期化スクリプト(preload)も設定できるため、生成したいファイルは複数必要になりますが、残念ながら@vercel/nccは複数のエントリーポイントを扱うのが得意ではないため、ここではバンドルをせずにtscで処理をしています。外部ライブラリを利用する場合などはメインプロセスもnccでバンドルを作成する方が良いでしょう。
127+
128+
もう一つのポイントは"browser"と"main"です。生成したJavaScriptを元に、ユーザーに配布しやすい形にランタイム込みのバンドルを作成するelectron-builderはmainの項目を見てビルドを行います。また、Parcelも同じくデフォルトでmainを見ますが、electron-builderのmainはメインプロセス、Parcelで処理をするのはレンダープロセス側です。そのため、parcelコマンドのオプションで、mainじゃない項目(ここではbrowser)に書かれたファイル名で出力するように--targetオプションを設定しています。
129+
130+
.. code-block:: json
131+
:caption: package.json
132+
133+
{
134+
"browser": "dist/index.html",
135+
"main": "dist/main.js",
136+
"scripts": {
137+
"serve": "parcel serve src/render/index.html",
138+
"build": "run-p build:main build:render",
139+
"build:main": "tsc -p tsconfig.main.json",
140+
"build:render": "parcel build --dist-dir=dist --public-url --target=browser \"./\" src/render/index.html",
141+
"start": "run-s build start:electron",
142+
"start:electron": "electron dist/main/index.js"
143+
}
144+
}
145+
146+
次のコマンドで開発を行っていきます。
147+
148+
* ``npm run serve``: フロントエンド部分をブラウザ上で実行します。
149+
* ``npm run build``: レンダープロセス、メインプロセス2つのコードをビルド
150+
* ``npm start``: ビルドした結果をelectronコマンドを使って実行
151+
152+
配布用アプリケーションの構築
153+
-------------------------------------
154+
155+
これまで作ってきた環境は開発環境で、Electron本体をnpmからダウンロードして実行します。エンドユーザー環境にはnpmもNode.jsもないことが普通でしょう。Electronの本体も一緒にバンドルしたシングルバイナリのアプリケーションを作成していきます。ビルドにはelectron-builderを利用します。
156+
157+
* https://www.electron.build/
158+
159+
インストールはnpmで行います。
160+
161+
.. code-block:: bash
162+
163+
npm install --save-dev electron-builder
164+
165+
electron-builderの設定はpackage.jsonに記述します。outputフォルダを設定しないとdistに出力され、Parcelなどの出力と最終的なバイナリが混ざり、2回目以降のビルド時にその前までにビルドした結果のファイルまでバンドルされてしまってファイルサイズがおかしなことになるため、distと別フォルダを設定します。
166+
167+
.. code-block:: json
168+
:caption: package.json
169+
170+
{
171+
"scripts": {
172+
"electron:build": "run-s build electron:bundle",
173+
"electron:bundle": "electron-builder"
174+
},
175+
"build": {
176+
"appId": "com.example.electron-app",
177+
"files": [
178+
"dist/**/*",
179+
"package.json"
180+
],
181+
"directories": {
182+
"buildResources": "resources",
183+
"output": "electron_dist"
184+
},
185+
"publish": null
186+
}
187+
}
188+
189+
次のコマンドで配布用のバイナリが作成できます。
190+
191+
* ``npm run electron:build``
192+
193+
これは本当の最小限です。electron-builderを利用すると、アイコンをつけたり、署名をしたりもできますし、クロスビルドも行えます。
194+
195+
デバッグ
196+
-----------------------------
197+
198+
普段のブラウザでは開発者ツールを開かないことにはconsole.logも利用できません。Electronもレンダープロセスのデバッグには開発者ツールが使いたくなるでしょう。開発者ツールを起動するには1行書くだけで済みます。環境変数やモードを見て開くようにすると便利でしょう。
199+
200+
.. code-block:: ts
201+
202+
win.webContents.openDevTools();
203+
204+
レンダープロセスとメインプロセス間の通信
205+
--------------------------------------------------------
206+
207+
レンダープロセスは通常のブラウザに近いものと紹介しましたが、セキュリティの考え方もほぼ同様です。Electronではブラウザウインドウを開くときにどのページを開くかを指定しましたが、ここでは外部のサービスを開くこともできます。普段はローカルのファイルで動くが、リモートのサービスも使えるブラウザです。
208+
209+
.. code-block:: ts
210+
211+
win.loadURL(`https://google.com`);
212+
213+
ただし、このリモートのサービスが使える点がElectronのセキュリティを難しいものにしています。Electronには、レンダープロセスでNode.jsの機能が使えるようになるnodeIntegrationという機能があり、ブラウザウインドウを開くときのオプションで有効化できます。しかしこれを有効化すると、ローカルのユーザー権限で見られるあらゆる場所のファイルにアクセスできますし、ファイルを書き換えたりできてしまい、クロスサイトスクリプティング脆弱性を入れ込んでしまうときのリスクが極大化されてしまうため、レンダープロセスが外部のリソースをロードする場合はこの機能はオフにすべきです(現在のデフォルトはオフです)。OpenID Connectの認証など、外部のリソースをロードしたいことはよくあるので、この機能はもうなかったものとして考えると良いでしょう。
214+
215+
代わりに提供されているのがコンテキストブリッジになります。歴史的経緯などは次のページにまとまっています。
216+
217+
* Electron(v10.1.5現在)の IPC 通信入門 - よりセキュアな方法への変遷: https://qiita.com/hibara/items/c59fb6924610fc22a9db
218+
219+
まず、ウィンドウを開くときのオプションで、nodeIntegrationをオフに、contextIsolationをオンにします。後者は、これからロードするプリロードのスクリプトが直接ブラウザプロセスの情報にアクセスできないようになります。
220+
221+
.. code-block:: ts
222+
:caption: main.ts
223+
224+
const win = new BrowserWindow({
225+
webPreferences: {
226+
nodeIntegration: false,
227+
contextIsolation: true,
228+
preload: __dirname + '/preload.js'
229+
}
230+
});
231+
232+
次に、レンダープロセスにAPIを追加します。preloadスクリプトを使うことで、レンダープロセスのグローバル変数に関数を追加できます。ここでは、\ ``window.api.writeFile()``\ という関数を定義しています。このスクリプトは2つのプロセスの中間地点です。ブラウザプロセスとは別のコンテキストで実行されます。どちらかというと、レンダープロセス寄りですが、レンダープロセスの内では直接扱えない機能が利用できます。\ ``ipcRenderer``\ が、メインプロセスとレンダープロセス間の通信を行うオブジェクトです。このコンテキストブリッジ内で\ ``ipcRendererの\ ``send()``\ \ ``on()``\ を呼び出すことで、メインプロセスに対する送信と受信が実現できます。
233+
234+
.. code-block:: ts
235+
:caption: preload.ts
236+
237+
// eslint-disable-next-line
238+
const { contextBridge, ipcRenderer } = require('electron');
239+
contextBridge.exposeInMainWorld('api', {
240+
writeFile: (data) => {
241+
ipcRenderer.send('writeFile', data);
242+
},
243+
})
244+
245+
``ipcRenderer``\ と対になる\ ``ipcMain``\ を使って通信を行います。
246+
247+
.. code-block:: ts
248+
:caption: main.ts
249+
250+
import { app, ipcMain } from 'electron';
251+
import { writeFileSync } from 'fs';
252+
import { join } from 'path';
253+
254+
ipcMain.on('writeFile', (_event, data) => {
255+
const jsonStr = JSON.stringify(data, null, 4);
256+
writeFileSync(join(app.getPath('userData'), jsonStr, 'utf8');
257+
});
258+
259+
これにより、ブラウザプロセス側には間接的にファイル読み書きを行うAPIを登録し、それ経由で、実際の危険な操作をともなうメインプロセス側の処理を呼び出すことが可能です。
260+
261+
.. list-table:: Electronのプロセス間通信
262+
:header-rows: 1
263+
264+
- * 通信方向
265+
* レンダープロセス側
266+
* メインプロセス側
267+
- * レンダープロセス→メインプロセス
268+
* ``ipcRenderer.send()``
269+
* ``ipcMain.on()``\ に登録したコールバック
270+
- * メインプロセス→レンダープロセス
271+
* ``ipcRenderer.on()``\ に登録したコールバック
272+
* ``ipcMain.send()``
273+
   
274+
まとめ
275+
------------
276+
277+
Electronについて、環境の構築から配布用バイナリの作成、Electronならではの開発のトピックを紹介してきました。近年のデスクトップアプリケーションの開発ではかなり人気のある選択肢となっています。TypeScriptとブラウザのアプリケーションの知識があればデスクトップアプリケーションが作成できます。フロントエンド系の開発者にとっては福音と言えるでしょう。
278+
279+
ChromeベースのEdgeが利用できるようになって、ブラウザ間の機能差は小さくなりましたが、Electronはすべてのユーザーに同一バージョンの最新ブラウザを提供するようなものでもあるため、社内システム開発でも使いたいというニーズはあるでしょう。また、ファイルシステムアクセスなど、ブラウザだけでは実現できない機能もいろいろ利用できます。
280+
281+
一方で、ツールバー、トレイなど、デスクトップならではのユーザビリティも考慮する必要は出てきますし、メニュー構成もWindows標準とmacの違いなどもあったりもします。フロントエンドの開発だけではなく、違和感なく使ってもらえるアプリケーションにするには、プラスアルファの手間隙がかかることは忘れないようにしてください。

index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
browserobjects
7272
react
7373
vue
74+
webpercel
75+
electron
7476

7577
.. toctree::
7678
:maxdepth: 2

0 commit comments

Comments
 (0)