🍖

Angular+Electronのテンプレートを作成しました。

2023/09/03に公開

概要

Angularでデスクトップアプリを作成するべく、Angular+Electronのテンプレートを作成しました。
当記事のコードで実装していることを以下に示します。

実装していること

  • ライブリローディングを実装
    • メインプロセスの変更を検知した場合、Electronを再起動します。
    • レンダラープロセス変更時、自動でリロードします。
      これはAngularの既存の機能です。
  • OSの機能にアクセス
    Node.jsの機能を介してOSの機能を使用する方法を解説しています。
    サンプルでは、仮のユーザーデータを保存したjsonファイルを読込み、Angularで表示させることで、OSの機能へのアクセスを示しています。
    また機能の拡張方法を解説しています。
  • 配布可能ファイルの作成方法
    セットアップファイルを作成し、配布する方法、Zip形式で配布する方法を解説しています。
  • GitHub Releaseでの公開方法、アプリの自動更新方法
    成果物をGitHub Releaseに公開する方法、アプリのバージョンアップ後の自動更新方法を解説しています。

参考記事、リポジトリ

当記事は以下の記事、リポジトリに多大なる恩恵を授かっています。

環境

Windows10
Node.js 16.15.0

使用方法

準備

  1. コードの取得
    下記コマンドでリポジトリを任意の場所にクローン、もしくは下記URLからコードを取得してください。
    git clone https://github.com/YamaDash82/angtron.git 
    
    GitHub YamaDash82/angtron
  2. パッケージのインストール
    npm install
    

開発時の操作方法

  1. 開発モードで実行

    npm run serve
    

    以下のような画面が表示されます。
    画面イメージ
    表示されている氏名一覧はmock/users.jsonの内容を読み込んで表示しています。
    Electronを用いてNode.jsを介してOSの機能を使ってテキストファイルを読み込んで表示しているわけです。

  2. メインプロセス修正後のライブリローディングを確認
    メインプロセス側のコードに変更を加えてみます。
    projects/main/src/main.ts

    function createWindow() {
      console.log(`テスト`); //←追加
      const isDevMode: boolean = !!process.argv.find(val => val === '--development');
      ...
    

    上書き保存してみてください。Electronウィンドウが再起動し、修正が適用されます。

  3. レンダラープロセス修正後のライブリローディングを確認
    ※レンダラープロセスの修正後のライブリローディングは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とします。内容は何でもよいです。

mock/hoge.txt
From Hello With Electron(^^)V
  1. メインプロセスにメソッド追加
    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');
    };
    
  2. メインプロセスでipcMain.handleでイベントをリッスン
    main/src/main.ts

    main/src/main.ts
    ...
    app.whenReady().then(async () => {
      ...
      ipcMain.handle('readTextFile', readTextFile)
      ...
    });
    ...
    
  3. プリロードスクリプトでipcRenderer.invokereadTextFileを公開
    main/src/preload.ts

    main/src/preload.ts
    contextBridge.exposeInMainWorld('fileAPI', {
      readTextFile: (targetPath: string): Promise<string> => {
        return ipcRenderer.invoke('readTextFile', targetPath)
      }, 
    });
    
  4. 型定義ファイルを修正
    types/preload.d.ts

    export interface FileAPI {
      readTextFile: (targetPath: string) => Promise<string>;
    }
    
    declare global {
      interface Window {
        ...
        fileAPI: FileAPI;
      }
    }
    

    以上でレンダラープロセスでwindow.fileAPI.readFileText()が使用できるようになります。

  5. レンダラープロセスの修正
    ではレンダラープロセスに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要素で指定したファイルを読込ボタン押下時に読込、下部に読み込んだ内容を表示している様子です。
    使用例

配布可能ファイルの作成

配布可能ファイルの作成手順を示します。

  1. アイコンの設定等
    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で設定したicondescriptionが反映しています。
      デスクトップの起動ショートカット
      package.jsonで設定したauthor
    • スタートメニューの起動アイコン
      package.jsonauthorで設定したフォルダが作成され、その下に起動ショートカットが配置されました。当方の環境ではforge.config.jsonauhtorsの設定は反映せず、package.jsonauthorが反映しました。
      スタートメニューの起動アイコン
    • インストーラーのアイコン
      forge.config.jsで設定したsetupIconが反映しています。
      インストーラーのアイコン
  2. ビルド
    以下のコマンドを実行します。

    npm run build
    

    プロジェクトフォルダにdistフォルダが作成され、ビルドにより生成されたファイルが配置されます。

  3. 配布可能ファイルの作成
    以下のコマンドを実行します。
    完了までに数分かかります。

    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の設定箇所は以下です。
    参考URL

    forge.config.js
    module.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形式は、任意の場所に展開して使用することができます。スタートメニューや、デスクトップにはショートカットは作成されません。

アプリの公開、自動更新

GitHubReleseにアプリを公開する手順を示します。
※事前にGitHubアカウントと、成果物をGitHubリポジトリに公開することが必要です。
参考URL

上記参考URLで、必須要件に"ビルドがコード署名されている"というものがありましたが、当方コード署名ないまま進めても公開することができました。
ただし、コード署名無しの影響と思われますが、セットアップを実行すると以下の警告画面が表示されます。
警告画面
"詳細情報"、"実行"の順にクリックすることで、セットアップを完了させることができます。
警告

  1. forge.config.jsの設定
    以下のGitHubアカウント名、リポジトリ名の部分を自身の環境に合わせて設定してください。

    forge.config.js
    module.exports = {
      ...
      publishers: [
        {
          name: '@electron-forge/publisher-github', 
          config: {
            repository: {
              owner: 'GitHubアカウント名', 
              name: 'リポジトリ名'
            }, 
            prerelease: false,
            draft: true, 
          }, 
        }, 
      ], 
    };
    
  2. npm run publishの実行
    下記コマンド実行によりGitHub Releaseにアプリがアップロードされます。
    ※アップロードのみでまだ公開はされていません。

    npm run publish
    
  3. GitHubでリリース
    アプリを公開するGitHubアカウント、リポジトリの画面を開き、画面右のReleasesをクリックします。
    GitHub画面
    該当のリリースの編集ボタンをクリックします。
    GitHub画面、リリース
    画面下の方に移動しPublish releaseをクリックします。
    GitHub画面 リリース
    これでアプリが公開されました。
    GitHub Releaseからセットアップファイルがダウンロードできます。
    セットアップファイルをダウンロードするリンクをコピーして、他のホームページのダウンロードボタンにリンクさせたりするのも良いと思います。
    セットアップファイルダウンロード

  4. 自動更新
    ユーザーが実行するアプリは、起動時に新しいバージョンがないか確認します。
    新しいバージョンが公開されていればバックグランドでそれをダウンロードし、ダウンロード完了後、アプリを再起動するか確認画面が表示されます。
    アップデート確認
    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   
  ...
  1. プロジェクトフォルダの作成
    任意の場所にプロジェクトフォルダを作成し、VSCodeで開きます。

  2. @angular/cliをインストール
    プロジェクトフォルダで@angular/cliをインストールします。
    ※当方@angular/cliをグローバルではなくローカルにインストールする方が好みです。

    npm install --save-dev @angular/cli
    

    プロジェクトフォルダにnode_modulesフォルダとpackage.jsonpackage-lock.jsonが作成されます。
    次の手順でng newコマンドで展開先を当フォルダに指定してプロジェクトを展開します。その際、package.jsonpackage-lock.jsonがあるとng newコマンド実行時にエラーになるので、@angular/cliインストール時に生成されたpackage.jsonpackage-lock.jsonを削除し、node_modulesフォルダだけの状態にします。

  3. Angular初期アプリケーションの作成
    まず初期アプリケーションのない空のワークスペースを作成します。
    Angularに必要なファイルが展開されます。

    npx ng new angtron-workspace --create-application false --directory .
    

    続いて、初期アプリケーションを展開します。
    下記コマンドでワークスペースにprojects/rendererというフォルダを作成し、初期アプリケーションを展開します。

    npx ng generate application renderer
    
  4. Angularプロジェクトに型定義ファイルへの参照を設定
    projects/renderer/tsconfig.app.jsonに、型定義ファイルへの参照を追加します。

    projects/renderer/tsconfig.app.json
    {
      ...
      "include": [
        "src/**/*.d.ts", 
        "../types/**/*.d.ts"  //追加
      ]
    }
    
  5. メインプロセスプロジェクトを作成
    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"
      ]
    }
    
  6. メインプロセスに初期コードを準備

    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'));
      }
    }
    
    1. 開発時か否かの取得

      const isDevMode: boolean = !!process.argv.find(val => val === '--development');
      

      この部分で開発モードか、本番モードかを区別する値を取得しています。
      後述するpackage.jsonscriptsの設定と組み合わせています。実行コマンドの引数に--developmentが含まれている場合、isDevModeフラグがtrueになります。isDevModeが真とみなされるとき、開発モードです。

    2. 開発モードフラグで分岐
      描画する内容を開発モードフラグ(isDevMode)で分岐しています。

      if (isDevMode) {
        //開発時
        ...
        win.loadURL('http://localhost:4200');
        ...
      } else {
        //本稼働時
        win.loadFile(path.join(__dirname, 'renderer/index.html'));
      }
      

      描画する内容を開発時は、Angularの仮サーバ―localhost:4200から取得し、本稼働時はビルドされた内容を取得します。

    3. デバッグツールの設定
      以下のライブラリを使用しています。

      • electron-debug
        sindresorhus/electron-debug
        開発時、以下の機能が有効になります。
        • DevToolsのトグル
          ctrl+Shift+IもしくはF12
        • 強制リロード
          ctrl+rもしくはF5
      • 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 {
          ...
        }
        ...
      }
      
  7. package.jsonscriptsを設定
    コマンドを同時実行させるためにnpm-run-allライブラリ、条件を満たすまで待機させるwait-onライブラリを使用するのでインストールします。
    (GitHub)mysticatea/npm-run-all
    (GitHub)jeffbski/wait-on

    npm 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.ts
        const isDevMode: boolean = !!process.argv.find(val => val === '--development');
        

    以上により、Electron開発時のライブリローディングを実装しています。

  8. Electron Forgeの導入
    (Electron Forge公式)Importing an Existing Project
    プロジェクトフォルダにelectron-forgeをインストールします。

    npm install --save-dev @electron-forge/cli
    

    続いてelectron-forgeimportコマンドを実行して、プロジェクトに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"
      }, 
      ,,,
    }
    
  9. 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 Project

    Handling 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.ts
    if (require('electron-squirrel-startup')) app.quit();
    
  10. 自動アップデート用の設定
    GitHub Releaseを使ってアプリを公開し、その後、新しいバージョンを公開したときにユーザーのアプリが自動更新されるようにします。
    参考URL

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

    npm install update-electron-app
    

    メインプロセスの早い段階に以下のコードを追加します。

    main.ts
    require('update-electron-app')();
    

以上です。

Discussion