UI を Angular、 DB を SQLite とした Electron をベースのアプリケーションを作ってみた。サンプルソースは https://github.com/nakamurakko/angular-electron-sqlite-sample にアップロード。
環境
- Angular 17
- Electron 28
- SQLite3 5
- TypeORM 0.3
- reflect-metadata 0.2
プロジェクトの準備
ng new
で Angular 新規プロジェクトを作成後、 npm install --save-dev electron
で Electron のパッケージを追加する。 DB に SQLite、 OR マッパーに TypeORM を使用するので npm install sqlite3 typeorm
で追加する。 TypeORM の インストールの説明を確認すると reflect-metadata のインストールが必要と記載があるため npm install reflect-metadata
で追加する。
データアクセスの大まかなイメージは下記の通り。
Electron 側の設定
Electron の起動構成
Electron 関連のソースファイルの保存場所は、 src-electron とした。
Electron のクイック スタートを参考に electron-main.ts を作成する。(main.ts だと Angular のファイル名が重複するので、 Electron 側の main.ts は electron-main.ts とした。)
createWindow()
で、Angular の index.html を読み込むように変更した。
await mainWindow.loadFile(path.join(__dirname, 'angular-electron-sqlite-sample/index.html'));
Entity クラス
DB(SQLite)アクセスするフレームワークに TypeORM を使用する。 SQLite へのアクセスは Electron 側から行うので、 SQLite 用のエンティティクラスは src-electron\entities で管理する。
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; /** * ユーザー Entity クラス。 */ @Entity() export class User { @PrimaryGeneratedColumn('uuid') public id?: string; @Column() public firstName?: string; @Column() public lastName?: string; @Column({ nullable: true }) public portrait?: string; }
User クラスは DataSource に Entitiy として登録する。
export const AppDataSource: DataSource = new DataSource({ type: "sqlite", database: "database.sqlite", synchronize: true, logging: false, entities: [User], migrations: [], subscribers: [], });
Entitiy インターフェイス
Electron > Electron のプロセス > プロセス間通信 > オブジェクトのシリアライズにある通り、 Angular - Electron 間は構造化複製アルゴリズムに記載された型でやりとりする。つまり、 TypeORM の Decorator を追加した Entity クラスではやりとりできないため、 Entity クラスと同じプロパティを持っているインターフェイス(ここでは Entitiy インターフェイスと呼ぶ事にする。)を使ってやりとりする。
まず、 User テーブルと対になる IUser インターフェイスを作成する。
/** * ユーザー Entitiy インターフェイス。 */ export interface IUser { /** ID。 */ id?: string; /** 名。 */ firstName?: string; /** 姓。 */ lastName?: string; /** 写真。 Data URL + base64 データで文字列化する。 */ portrait?: string; }
ディレクトリーはプロジェクト全体で参照できるように TypeScript > Publish to @types を参考にして src/@types/entities/interfaces とした。(Electron 側でインポートする場合に相対パスになって若干面倒くさいけど、インターフェイスの定義を統一できるメリットを優先する。)
作成したインターフェイスを、先ほど作成した User クラスの implements に追加する。
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { IUser } from '../../src/@types/entities/interfaces/i-user'; /** * ユーザー Entity クラス。 */ @Entity() export class User implements IUser { @PrimaryGeneratedColumn('uuid') public id?: string; @Column() public firstName?: string; @Column() public lastName?: string; @Column({ nullable: true }) public portrait?: string; }
プロセス間通信を利用した DB アクセス
Angular 側からデータ取得処理を呼び出せるように、 Electron > プロセス間通信 > パターン 2: レンダラーからメインへ (双方向)を参考に DB アクセスを実装する。
まずは ipcMain.handle
で SQLite にアクセスしてデータを取得し、取得結果をインターフェイスで返す IPC(プロセス間通信) Handler を実装する。 channel 名は getUsers とした。
ipcMain.handle('getUsers', /** * ユーザー一覧を取得する。 * * @returns ユーザー一覧。 */ async (): Promise<Array<IUser>> => { return await AppDataSource.getRepository(User) .find() .then(value => value.map(x => x as IUser)); });
preload.js
で公開する。 apiKey は dbApi、 channel 名は getUsers とした。
import { contextBridge, ipcRenderer } from 'electron'; import { IUser } from '../src/@types/entities/interfaces/i-user'; contextBridge.exposeInMainWorld('dbApi', { /** * ユーザー一覧を取得する。 * * @returns ユーザー一覧。 */ getUsers: (): Promise<Array<IUser>> => ipcRenderer.invoke('getUsers').then(value => value as Array<IUser>) });
最後にコンテキストの分離 > TypeScript との連携を参考に、 renderer.d.ts を用意して、公開した API を Angular 側に参照させる。 renderer.d.ts の実装が完了すれば Angular 側から呼び出せるようになる。
import { IUser } from 'src/@types/entities/interfaces/i-user'; declare global { interface Window { dbApi: IDbApi } } /** * dbApi 用インターフェイス。 */ export interface IDbApi { /** * ユーザー一覧を取得する。 * * @returns ユーザー一覧。 */ getUsers(): Promise<Array<IUser>>; }
Angular 側の設定
Electron で用意した DB アクセスは Promise で実装しているけど、 Angular 内では基本的に Observable を使いたいので、 DB API アクセス用 Service を用意して、 Observable に変換する。
import { defer, Observable } from 'rxjs'; import { IUser } from 'src/@types/entities/interfaces/i-user'; import { Injectable } from '@angular/core'; /** * DbApi 用サービス。 */ @Injectable({ providedIn: 'root' }) export class DbApiService { public constructor() { } /** * ユーザー一覧を取得する。 * * @returns ユーザー一覧。 */ public getUsers(): Observable<Array<IUser>> { return defer(() => window.dbApi.getUsers()); } }
後は画面を実装して、呼び出せば良い。
import { IUser } from 'src/@types/entities/interfaces/i-user'; import { DbApiService } from 'src/app/services/db-api.service'; import { Component, OnInit } from '@angular/core'; /** * ユーザー情報表示用コンポーネント。 */ @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.css'] }) export class UserComponent implements OnInit { /** 一覧のヘッダー。 */ public displayedColumns: Array<string> = ['lastName', 'firstName', 'showUserDetail', 'editUser']; /** ユーザー一覧。 */ public users: Array<IUser> = new Array<IUser>(); /** * コンストラクター。 * * @param dbApiService DB サービス。 */ public constructor( private dbApiService: DbApiService ) { } public ngOnInit(): void { this.progressService.showProgress(); this.dbApiService.getUsers() .subscribe(value => { this.users = value; }); } }
README を参考にデバッグすれば、下記の通り表示される。