[Node.js] Electron、それは果てしなく「今日は世界！」が続く世界

# Electron、それは果てしなく「今日は世界！」が続く世界

## 0. 作ったもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/19c39700-6219-4205-949b-ca925c07ce15.png)


## 1．やりたかったのはWebアプリをWindowsのデスクトップアプリへ移植すること

　Node.jsを使い、MyDNSのユーザ情報を管理する[Webアプリケーション](https://github.com/ActiveModuleFramework/MyDNS-Explorer)を作成しました。しかし他人のユーザ情報を管理するのはさすがに怖いので、サービスとしては公開していませんでした。いっそデスクトップアプリにしてしまえば問題ないと思い立ち、Electronを使いパッケージ化することにしました。

## 2．Electronの利用方法を調べると、みんな挨拶しかしていない

　Node.jsをパッケージ化するための方法はいくつかありますが、一番まともそうな方法はElectronを使うことでした。ということで情報を調べていくと、ことごとくが「今日は世界」を表示するところで終わっています。「おまえら、どれだけ挨拶すれば気が済むんだ？」と心の中で叫ぶしかありませんでした。しかも、挨拶ラッシュが起こってからけっこう時代が進んでいるらしく、パッケージ化の情報が新旧入り交じっていました。

## 3．とにかく簡単に移植したい

　Electronをがっつり使いたいわけではありません。実質、ローカルWebServerと専用ブラウザとしての機能が使えれば良いのです。Electronでメインととレンダラを繋ぐためのプロセス間通信モジュールとかありますが、それを使った時点でWebアプリの移植に余計な改変を加えなければなりません。一切使わない方針を決めました。普通にAjaxが使えるんだから、移植だったらそのままいけば良いだけです。欠点があるとすればTCPのポートを一個、起動時に用意しなければならないぐらいです。

## 4．SQLite3の恐怖

　Electronはネイティブのライブラリが含まれているモジュールをそのまま動かしてくれないようです。大半の人が引っかかるであろうモジュールは、SQLite3だと思われます。ということで専用ビルドをかけなければなりません。コンパイルに必要な設定は以下の通りです。

- Python2.7系統にpathを通す(先頭に記述しないとStoreが起動)
- VisualStudioのインストール
  - VS2019の場合
    - **C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin** にパスを通す
    - インストーラを起動してVS2015のビルドツールの追加
- npm install sqlite3 --build-from-source --save --runtime=electron --target=5.0.1 --dist-url=https://atom.io/download/electron



## 5. メインプロセスのconsole.logの文字化け

　Electronの動作をPowerShellから実行するとそのままでは**console.log**で文字化けします。対処法はElectron実行前に、

```
chcp 65001
```
で、文字コードをUTF8にすることです。Electron利用時のconsole.logはデバッグ出力をするためなので、実際のところ日本語を出さなければ話は簡単です。

## 6. パッケージの作成

　以下の設定を**package.json**に追加し、**electron-builder**を実行します。**electron-packager**の方は設定が不便なので使いません。

```package.json
{
…
  "build": {
    "productName": "出力アプリケーション名",
    "appId": "hogehoge-hoge",
    "directories": {
      "output": "build"
    },
    "files": [
      "dist/**/*"
    ],
    "win": {
      "target": "nsis"
    }
  },
…  
}
```

## 7. Webアプリ移植時に追加するコード

　余計な改変を最小限にするためバックエンド起動処理の**index.ts**に、Electronのウインドウ表示処理を入れています。本来のWebアプリはバックエンドの処理のみを記述していました。この部分以外、Electron専用のソースは書いていません。

```index.ts
import * as amf from 'active-module-framework'
import * as electron from 'electron'
import * as path from 'path'

const listenPort = 58621

/**
 * Electron用起動設定
 */
const app = electron.app
let window: electron.BrowserWindow|null = null
//Electronで実行されているかとUNIXドメインソケットを使用していないか確認
if (app) {
	//ウインドウが閉じられた場合の処理
	app.on("window-all-closed", async () => {
		if (process.platform != "darwin") {
			await manager.destory() //バックエンド処理を終了させる
			app.quit()				//アプリケーション終了
		}
	})
	//ウインドウ表示処理
	const init = () => {
		window = new electron.BrowserWindow({ width: 1280, height: 720, autoHideMenuBar: true })
		//起動メッセージの表示
		window.loadURL(`file://${path.resolve(__dirname,'../template/electron.html')}`)
	}
	//準備完了時に初期化
	if (app.isReady()) {
		init()
	} else {
		app.once("ready", () => {
			init()
		})
	}
}


/**
 * バックエンドが準備完了になった場合の処理
 */
const listened = (port: number | string|null)=> {
	if (window){
		if (port === null){
			//ポート使用中のエラー表示
			window.loadURL(`file://${path.resolve(__dirname, '../template/electron.html')}?cmd=error&port=${listenPort}`)
		}
		else if(typeof port === 'number'){
			window.loadURL(`http://localhost:${port}`)
		}
	}
}


/**
 * バックエンドの設定
 */
const manager = new amf.Manager({
	remotePath: '/',										//一般コンテンツのリモートパス
	execPath: '/',											//コマンド実行用リモートパス
	indexPath: path.resolve(__dirname, '../template/index.html'),//index.thmlテンプレート
	rootPath: path.resolve(__dirname, '../public'),			//一般コンテンツのローカルパス
	cssPath: ['css'],										//自動ロード用CSSパス
	jsPath: ['js'],											//一般コンテンツのローカルパス
	localDBPath: path.resolve(__dirname,'../db/app.db'),	//ローカルDBパス
	//localDBPath: path.resolve('app.db'),	//ローカルDBパス(カレントパスに設定)
	modulePath: path.resolve(__dirname, './modules'),		//モジュール配置パス
	jsPriority: [],											//優先JSファイル設定
	debug: false,											//デバッグ用メッセージ出力
	listened,												//初期化完了後コールバック
	//listen: listenPort									//受付ポート/UNIXドメインソケット
	listen: path.resolve(__dirname, '../sock/app.sock')		//UNIXドメインソケットを使用する場合
})

```

　ウインドウを表示後にバックエンド処理がlisten可能になったらページを切り替えています。Electronから起動していない場合は**app**にインスタンスが入らないので、Electronがらみの処理を行いません。こうやって書いておけば、通常のNodeプロセスとして起動しても動きます。その場合、通常のWebアプリモードとなります。


## 8. ビルド手順

　package.jsonの**script**が以下のようになっています。フロントエンドはWebPack、バックエンドはtsc、パッケージ化はelectron-builderを利用します。さらにSQLite3のインストールを忘れないように書いておきました。

```package.json
{
･･･
  "scripts": {
    "start": "node dist/app/index.js ",
    "watch": "tsc -b -w",
    "build-app": "tsc -b",
    "build-front": "npx webpack",
    "build-electron": "npx electron-builder --win",
    "install-sqlite": "npm install sqlite3 --build-from-source --save --runtime=electron --target=5.0.1 --dist-url=https://atom.io/download/electron",
    "run": "electron ."
  },
･･･
}
```

　手数が多いというか、使っているものがどんどん増えていく感じがなんとも言えません。

## 9. 使っているもの

- フロントエンドフレームワーク (オレオレフレームワーク)
  - [javascript-window-framework](https://www.npmjs.com/package/javascript-window-framework)
- バックエンドフレームワーク (オレオレフレームワーク)
  - [active-module-framework](https://www.npmjs.com/package/active-module-framework)
- パッケージ化
  - [electron](https://www.npmjs.com/package/electron)

　Webアプリを素早く作成するため、オレオレフレームワークで固めました。フロントエンドは有名どころのVue.jsやReactが、私としては使いにくかったので全部自作です。

　Webアプリを作るときの理想は、フロントエンド側はJavaScriptを呼び出すための初期ページ以外はHTMLを書かないことです。バックエンド側の理想は、初期ページの吐き出し以外に一切のHTMLを出力せず、データのやりとりのみを記述することです。

## 10． まとめ

　最近のWebプログラムは、エコシステムが発達したおかげで、コマンド一発で便利なライブラリやフレームワークが手に入ります。入門記事を見ながら進めれば、なんとなく雰囲気をつかむところまでは到達できることでしょう。しかし「今日は世界」を卒業しようとすると、途端に情報量が少なくなり、その先は茨の道が待っています。何をやろうにも必要とする技術が多岐に渡り、初学者が始めようとすると、そこそこの地獄を見ているのではないでしょうか？　

　結局のところ試行錯誤して、自分なりの正解を見つけるしかありません。用意されているものを敢えて使わないという選択肢もあるのです。プログラムはやりたいことを書いていけば、思った通りには動きませんが書いた通りには動きます。「今日は世界」を突破して、新たな世界を開拓しましょう。そこはそもそも挨拶の必要が無い、誰もいない不毛の土地かもしれませんが。