Angular+Electronのテンプレートを作成しました。
概要
Angularでデスクトップアプリを作成するべく、Angular+Electronのテンプレートを作成しました。
当記事のコードで実装していることを以下に示します。
実装していること
- ライブリローディングを実装
- メインプロセスの変更を検知した場合、Electronを再起動します。
- レンダラープロセス変更時、自動でリロードします。
これはAngularの既存の機能です。
- OSの機能にアクセス
Node.jsの機能を介してOSの機能を使用する方法を解説しています。
サンプルでは、仮のユーザーデータを保存したjsonファイルを読込み、Angularで表示させることで、OSの機能へのアクセスを示しています。
また機能の拡張方法を解説しています。 - 配布可能ファイルの作成方法
セットアップファイルを作成し、配布する方法、Zip形式で配布する方法を解説しています。 - GitHub Releaseでの公開方法、アプリの自動更新方法
成果物をGitHub Releaseに公開する方法、アプリのバージョンアップ後の自動更新方法を解説しています。
参考記事、リポジトリ
当記事は以下の記事、リポジトリに多大なる恩恵を授かっています。
環境
Windows10
Node.js 16.15.0
使用方法
準備
- コードの取得
下記コマンドでリポジトリを任意の場所にクローン、もしくは下記URLからコードを取得してください。GitHub YamaDash82/angtrongit clone https://github.com/YamaDash82/angtron.git
- パッケージのインストール
npm install
開発時の操作方法
-
開発モードで実行
npm run serve
以下のような画面が表示されます。
表示されている氏名一覧はmock/users.json
の内容を読み込んで表示しています。
Electronを用いてNode.jsを介してOSの機能を使ってテキストファイルを読み込んで表示しているわけです。 -
メインプロセス修正後のライブリローディングを確認
メインプロセス側のコードに変更を加えてみます。
projects/main/src/main.ts
function createWindow() { console.log(`テスト`); //←追加 const isDevMode: boolean = !!process.argv.find(val => val === '--development'); ...
上書き保存してみてください。Electronウィンドウが再起動し、修正が適用されます。
-
レンダラープロセス修正後のライブリローディングを確認
※レンダラープロセスの修正後のライブリローディングはAngular元来の機能です。
レンダラープロセス側(Angular)に修正を加えて上書き保存してみます。
現在日時が表示されるように修正します。
app.component.ts
... @Component({ ... template: ` ... <p>現在日時:{{currentDateTime.toLocaleString()}}</p> ←追加 ... ` ... }) export class AppComponent implements OnInit { ... currentDateTime = new Date(); //←追加 }
Electronウィンドウは起動したまま、表示部分が再表示され、修正の適用がされます。
機能拡張手順
ファイルアクセスなど、Node.jsの機能を用いたメソッドを追加し、Angularから呼び出す方法を示します。
(公式)プロセス間通信
readTextFile(targetFilePath)
という引数で指定したファイルパスのテキストファイルをリードするメソッドを追加するとします。画面でユーザーが指定したファイル名をreadTextFile
メソッドを用いて読込み、その内容を画面に表示させるものとします。つまりレンダラーでイベントを発火し、メインプロセスでそれを受けて処理を行い、レンダラーに返します。
リード対象の仮データをmock/hoge.txt
とします。内容は何でもよいです。
From Hello With Electron(^^)V
-
メインプロセスにメソッド追加
main/src/main.ts
にテキストファイルをリードするメソッドを追加します。main/src/main.ts... import * as fs from 'fs'; ... const readTextFile = async (event: Event, targetPath: string): Promise<string> => { //当例ではmockフォルダ配下のファイルを参照するようにしています。 return fs.readFileSync(path.join(__dirname, '../mock', targetPath), 'utf8'); };
-
メインプロセスで
ipcMain.handle
でイベントをリッスン
main/src/main.ts
main/src/main.ts... app.whenReady().then(async () => { ... ipcMain.handle('readTextFile', readTextFile) ... }); ...
-
プリロードスクリプトで
ipcRenderer.invoke
でreadTextFile
を公開
main/src/preload.ts
main/src/preload.tscontextBridge.exposeInMainWorld('fileAPI', { readTextFile: (targetPath: string): Promise<string> => { return ipcRenderer.invoke('readTextFile', targetPath) }, });
-
型定義ファイルを修正
types/preload.d.ts
export interface FileAPI { readTextFile: (targetPath: string) => Promise<string>; } declare global { interface Window { ... fileAPI: FileAPI; } }
以上でレンダラープロセスで
window.fileAPI.readFileText()
が使用できるようになります。 -
レンダラープロセスの修正
ではレンダラープロセスにreadFileTextメソッドの呼び出しを追加します。
renderer/src/app/app.component.ts
@Component({ ... template: ` ... <div> <input type="text" [(ngModel)]='targetPath' > <button (click)="readFile()">読込</button> <div *ngIf="readData">{{readData}}</div> </div> ... `, ... }) export class AppComponent implements OnInit { ... //読み込むファイルパス targetPath = `hoge.txt`; //読み込んだファイルの内容 readData = ''; ... readFile() { //テキストファイルを読み込む window.fileAPI.readTextFile(this.targetPath).then( data => this.readData = data ); } }
以上の流れでメソッドを追加します。
下図はinput要素で指定したファイルを読込ボタン押下時に読込、下部に読み込んだ内容を表示している様子です。
配布可能ファイルの作成
配布可能ファイルの作成手順を示します。
-
アイコンの設定等
forge.config.js
で以下の項目を設定します。項目 内容 icon ショートカットのアイコン authors 作成者 description アプリの説明 setupIcon セットアップファイルのアイコン const path = require('path'); module.exports = { packageConfig: { icon: path.join(__dirname, './assets/icon/icon.ico'), //ショートカットに設定されるアイコン }, makers: [ { name: '@electron-forge/maker-squirrel', config: { authors: 'hogehoge company', description: 'Angtronサンプルアプリケーション', setupIcon: path.join(__dirname, './assets/icon.ico'), //セットアップファイルに設定されるアイコン } }, ... ] }
package.json
で以下の項目を設定します。項目 内容 productName アプリ名 author 作成者 description アプリの説明 ※項目がforge.config.jsと重複しています。公式では一方の設定のみで良いようですが、当方の環境では作成者はpackage.jsonの方が適用されました。(公式)Electron Forge Squirrel.Windows
設定した内容が下図のように適用されます。
- デスクトップの起動ショートカット
forge.config.jsで設定したicon
とdescription
が反映しています。
package.jsonで設定したauthor
- スタートメニューの起動アイコン
package.json
のauthor
で設定したフォルダが作成され、その下に起動ショートカットが配置されました。当方の環境ではforge.config.json
のauhtors
の設定は反映せず、package.json
のauthor
が反映しました。
- インストーラーのアイコン
forge.config.js
で設定したsetupIcon
が反映しています。
- デスクトップの起動ショートカット
-
ビルド
以下のコマンドを実行します。npm run build
プロジェクトフォルダに
dist
フォルダが作成され、ビルドにより生成されたファイルが配置されます。 -
配布可能ファイルの作成
以下のコマンドを実行します。
完了までに数分かかります。npm run make
プロジェクトフォルダに
out
フォルダが作成され、配布可能ファイルが配置されます。
当テンプレートでは、squirrel.windows
フォルダ配下にWindows向けインストーラーと、zip
フォルダ配下にzip形式の配布ファイルが作成されるようにしています。設定はforge.config.js
で行っています。out\ ├make\ │ ├squirrel.windows\ │ │ └x64\ │ │ ├my_angtron_app-0.0.4-full.nupkg │ │ ├my-angtron-app-0.0.4 Setup.exe │ │ └RELEASES │ └zip\ │ └win32 │ └x64\ │ └my-angtron-app-win32-x64-0.0.4.zip └my-angtron-app-win32-x64\ ├... ...
forge.config.js
の設定箇所は以下です。
参考URLforge.config.jsmodule.exports = { ... makers: [ { name: '@electron-forge/maker-squirrel', config: { authors: 'hogehoge_forge_config', description: 'Angtronサンプルアプリケーション', setupIcon: path.join(__dirname, './assets/icon/icon.ico'), }, }, { name: '@electron-forge/maker-zip' } ], ... }
- Setup.exeによるインストール
インストーラーを実行すると、ユーザーフォルダにプログラムが配置され、デスクトップとスタートメニューに起動ショートカットが作成されます。※インストール先の指定は出来ません。方法があるかもしれませんが、現時点では見つけられませんでした。 - zip形式による配置
生成されたzip形式は、任意の場所に展開して使用することができます。スタートメニューや、デスクトップにはショートカットは作成されません。
- Setup.exeによるインストール
アプリの公開、自動更新
GitHubReleseにアプリを公開する手順を示します。
※事前にGitHubアカウントと、成果物をGitHubリポジトリに公開することが必要です。
参考URL
上記参考URLで、必須要件に"ビルドがコード署名されている"というものがありましたが、当方コード署名ないまま進めても公開することができました。
ただし、コード署名無しの影響と思われますが、セットアップを実行すると以下の警告画面が表示されます。
"詳細情報"、"実行"の順にクリックすることで、セットアップを完了させることができます。
-
forge.config.js
の設定
以下のGitHubアカウント名、リポジトリ名の部分を自身の環境に合わせて設定してください。forge.config.jsmodule.exports = { ... publishers: [ { name: '@electron-forge/publisher-github', config: { repository: { owner: 'GitHubアカウント名', name: 'リポジトリ名' }, prerelease: false, draft: true, }, }, ], };
-
npm run publish
の実行
下記コマンド実行によりGitHub Releaseにアプリがアップロードされます。
※アップロードのみでまだ公開はされていません。npm run publish
-
GitHubでリリース
アプリを公開するGitHubアカウント、リポジトリの画面を開き、画面右のReleases
をクリックします。
該当のリリースの編集ボタンをクリックします。
画面下の方に移動しPublish release
をクリックします。
これでアプリが公開されました。
GitHub Releaseからセットアップファイルがダウンロードできます。
セットアップファイルをダウンロードするリンクをコピーして、他のホームページのダウンロードボタンにリンクさせたりするのも良いと思います。
-
自動更新
ユーザーが実行するアプリは、起動時に新しいバージョンがないか確認します。
新しいバージョンが公開されていればバックグランドでそれをダウンロードし、ダウンロード完了後、アプリを再起動するか確認画面が表示されます。
Restart
ボタンをクリックすると、アプリが終了し、新しいバージョンで再起動します。
上記アップデート確認画面は自身でカスタマイズすることは出来ないようです。
当サンプルを作成した手順
プロジェクトフォルダの構成、フォルダを整えていく手順、tsconfigの設定は以下の記事を参考にさせていただきました。
(Qiita)Electron + Angular は、こうしたらいいんじゃないかなプラクティス by sengoku
(プロジェクトフォルダルート)
├...
├node_modules/
├projects/
│ ├main/
│ │ ├src/
│ │ │ ├main.ts
│ │ │ └preload.ts
│ │ └tsconfig.json
│ ├renderer/
│ │ ├src/
│ │ └tsconfog.app.json
│ └types/ # 型定義ファイル配置場所
├forge.config.json
├package.json
...
-
プロジェクトフォルダの作成
任意の場所にプロジェクトフォルダを作成し、VSCodeで開きます。 -
@angular/cli
をインストール
プロジェクトフォルダで@angular/cli
をインストールします。
※当方@angular/cli
をグローバルではなくローカルにインストールする方が好みです。npm install --save-dev @angular/cli
プロジェクトフォルダに
node_modules
フォルダとpackage.json
、package-lock.json
が作成されます。
次の手順でng new
コマンドで展開先を当フォルダに指定してプロジェクトを展開します。その際、package.json
、package-lock.json
があるとng new
コマンド実行時にエラーになるので、@angular/cli
インストール時に生成されたpackage.json
、package-lock.json
を削除し、node_modules
フォルダだけの状態にします。 -
Angular初期アプリケーションの作成
まず初期アプリケーションのない空のワークスペースを作成します。
Angularに必要なファイルが展開されます。npx ng new angtron-workspace --create-application false --directory .
続いて、初期アプリケーションを展開します。
下記コマンドでワークスペースにprojects/renderer
というフォルダを作成し、初期アプリケーションを展開します。npx ng generate application renderer
-
Angularプロジェクトに型定義ファイルへの参照を設定
projects/renderer/tsconfig.app.json
に、型定義ファイルへの参照を追加します。projects/renderer/tsconfig.app.json{ ... "include": [ "src/**/*.d.ts", "../types/**/*.d.ts" //追加 ] }
-
メインプロセスプロジェクトを作成
projects
フォルダ配下にmain
フォルダを作成し、メインプロセスの環境を整えます。
main/tsconfig.json
main/tsconfig.json{ "extends": "../../tsconfig.json", "compilerOptions": { "target": "es2020", "module": "commonjs", "lib": ["ES2020", "DOM"], "outDir": "../../dist", "types": [], "allowSyntheticDefaultImports": true, "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": [ "../../**/*.d.ts", "src/**/*.ts" ] }
-
メインプロセスに初期コードを準備
main.ts
のコードを一部抜粋します。... import debug from 'electron-debug'; //3. デバッグツールの設定 import electronReloader from "electron-reloader"; ... function createWindow() { const isDevMode: boolean = !!process.argv.find(val => val === '--development'); // 1. 開発時か否かの取得 const win = new BrowserWindow({ height: 600, webPreferences: { preload: path.join(__dirname, "preload.js"), }, width: 800, icon: path.join(__dirname, '../assets/icon/icon.ico') }); if (isDevMode) { //2. 開発フラグで分岐 //開発時 //electron-debugツールを実行する。 debug(); try { //electron-reloaderを実行する。 electronReloader(module, { watchRenderer: false //レンダラー側の変更は監視しない。 }); } catch { } win.loadURL('http://localhost:4200'); //開発ツールを開く。 win.webContents.openDevTools(); } else { //本稼働時 win.loadFile(path.join(__dirname, 'renderer/index.html')); } }
-
開発時か否かの取得
const isDevMode: boolean = !!process.argv.find(val => val === '--development');
この部分で開発モードか、本番モードかを区別する値を取得しています。
後述するpackage.json
のscripts
の設定と組み合わせています。実行コマンドの引数に--development
が含まれている場合、isDevMode
フラグがtrueになります。isDevModeが真とみなされるとき、開発モードです。 -
開発モードフラグで分岐
描画する内容を開発モードフラグ(isDevMode
)で分岐しています。if (isDevMode) { //開発時 ... win.loadURL('http://localhost:4200'); ... } else { //本稼働時 win.loadFile(path.join(__dirname, 'renderer/index.html')); }
描画する内容を開発時は、Angularの仮サーバ―
localhost:4200
から取得し、本稼働時はビルドされた内容を取得します。 -
デバッグツールの設定
以下のライブラリを使用しています。-
electron-debug
sindresorhus/electron-debug
開発時、以下の機能が有効になります。- DevToolsのトグル
ctrl
+Shift
+I
もしくはF12
- 強制リロード
ctrl
+r
もしくはF5
- DevToolsのトグル
-
electron-reloader
sindresorhus/electron-reloader
メインプロセスの変更を検知して、ウィンドウを再起動します。
以下の箇所で上記ライブラリを有効にしています。
... import debug from 'electron-debug'; import electronReloader from "electron-reloader"; function createWindow() { ... if (isDevMode) { //electron-debugツールを実行する。 debug(); try { //electron-reloaderを実行する。 electronReloader(module, { watchRenderer: false //レンダラー側の変更は監視しない。 }); } catch { } } else { ... } ... }
-
-
-
package.json
にscripts
を設定
コマンドを同時実行させるためにnpm-run-all
ライブラリ、条件を満たすまで待機させるwait-on
ライブラリを使用するのでインストールします。
(GitHub)mysticatea/npm-run-all
(GitHub)jeffbski/wait-onnpm install --save-dev npm-run-all wait-on
package.json
にスクリプトを追加します。{ ... "scripts": { ... "serve": "npm-run-all -p serve:ng serve-watch:el", "start:el": "electron .", "serve:el": "electron . --development", "build:el": "tsc --build projects/main/tsconfig.json", "watch:el": "tsc --build --watch projects/main/tsconfig.json", "serve-watch:el": "wait-on tcp:4200 && npm-run-all -p watch:el serve:el", "build:ng": "ng build --configuration production --base-href ./", "serve:ng": "ng serve", } }
npm run serve
コマンドを追跡して解説します。npm run serve
実行時、以下のコマンドを内部で実行します。
npm-run-all -p serve:ng serve-watch:el
これはnpm run serve:ng
コマンドとnpm run serve-watch:el
コマンドを並列実行しています。-
serve:ng
ng serve
を実行し、Anuglarの開発サーバーを起動します。localhost:4200
からAngularの開発中の画面が取得できるようになります。 -
serve-watch:el
npm run wait-on tcp:4200 && npm-run-all -p watch:el serve:el
を実行しています。
wait-on tcp:4200
により、Anglurの開発サーバーが起動するまで、以降のコマンドを待機します。
Angularの開発サーバーが起動するとnpm-run-all
により、npm run watch:el
コマンドとnpm run serve:el
コマンドを並列実行します。-
watch:el
tsc --build --watch projects/main/tsconfig.json
を実行し、メインプロセスをトランスパイルします。 -
serve:el
electron . --development
を実行し、Electronウインドウを起動します。
引数に指定した--development
を、メインプロセス中のコードで取得し、開発時か、本稼働時かをコードに分岐させています。
main.ts
main.tsconst isDevMode: boolean = !!process.argv.find(val => val === '--development');
-
以上により、Electron開発時のライブリローディングを実装しています。
-
-
Electron Forgeの導入
(Electron Forge公式)Importing an Existing Project
プロジェクトフォルダにelectron-forge
をインストールします。npm install --save-dev @electron-forge/cli
続いて
electron-forge
のimport
コマンドを実行して、プロジェクトにElecron Forge
を取り込みます。npx electron-forge import
import
コマンドについてThis command will attempt to take an existing Electron app and make it compatible with Forge. Normally, this just creates a base Electron Forge configuration and adds the required dependencies.
(google翻訳)
このコマンドは、既存のElectronアプリを取得して、Forgeと互換性を持たせることを試みます。通常、これは基本的なElectron Forge構成を作成し、必要な依存関係を追加するだけです。
(Electron Forge公式)CLIコマンドが成功すると、
package.json
の"scripts"
にコマンドの追加され、プロジェクトフォルダにforge.config.js
ファイルが作成されます。pacakge.json{ ... "scripts": { "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish" }, ,,, }
-
Windows用の調整
Electron Forge公式に以下のように記述があるので適用します。Adding Squirrel.Windows boilerplate
When distributing a Squirrel.Windows app, we recommend installing electron-squirrel-startup as a runtime dependency to handle Squirrel events.
(google翻訳)
Squirrel.Windows アプリを配布する場合、Squirrel イベントを処理するために、electron-squirrel-startup をランタイム依存関係としてインストールすることをお勧めします。
(Electron Forge公式)Importing an Existing ProjectHandling startup events
When first running your app, updating it, and uninstalling it, Squirrel.Windows will spawn your app an additional time with some special arguments. You can read more about these arguments on the electron-winstaller README.
The easiest way to handle these arguments and stop your app launching multiple times during these events is to use the electron-squirrel-startup module as one of the first things your app does.
(google翻訳)
起動イベントの処理
初めてアプリを実行し、更新し、アンインストールするときに、Squirrel.Windows はいくつかの特別な引数を使用してアプリを追加で生成します。 これらの引数の詳細については、electron-winstaller README を参照してください。
これらの引数を処理し、これらのイベント中にアプリの複数回の起動を停止する最も簡単な方法は、アプリが最初に行うことの 1 つとして Electron-squirrel-startup モジュールを使用することです。
(Electron Forge公式)Makers#Squirrel.Windows上記に倣い、メインプロセスの早い段階に以下のコードを追加します。
main.tsif (require('electron-squirrel-startup')) app.quit();
-
自動アップデート用の設定
GitHub Releaseを使ってアプリを公開し、その後、新しいバージョンを公開したときにユーザーのアプリが自動更新されるようにします。
参考URLモジュールのインストール
npm install update-electron-app
メインプロセスの早い段階に以下のコードを追加します。
main.tsrequire('update-electron-app')();
以上です。
Discussion