簡介 Intro 來介紹一下如何使用Electron開發框架,並且結合TypeScript與Angular設定
並且最終使用Electron-Forge打包成桌面應用程式。
Type-Safe IPC : Eliminates runtime errors by enforcing a strict contract for IPC channels, payloads, and return types between the main process and renderer processes. 
Simplified Utility Process Management : Provides a straightforward wrapper for creating, communicating with, and managing Electron’s utilityProcess. 
Angular-Friendly Service : Includes an example of an injectable Angular service (IPCRendererService) that wraps IPC calls in an RxJS-friendly way. 
 
 
如果你懶得看文章,也可以直接參考範例專案 
前置準備 在真正開始之前,需要先確保相關的開發環境準備
TypeScript 
Node.js 
Electron 
Electron-Forge 
ESLint 
 
如果你是在windows環境下,則建議還要多準備以下幾個
Wix Toolset 
Visual Studio Build Tools 
 
 
章節介紹 
資料結構宣告
文件結構 
共用參考結構 
Electron結構宣告 
Angular結構宣告 
Angular Service封裝 
 
 
不同的設定方法差別
使用git submodule維護前端與後端共同結構參考 (推薦) 
使前端或後端其中一方,屬於對方的文件結構之下 (稍微不推薦) 
使用tsconfig.json的paths與references來維護前端與後端的共同結構參考 (不推薦) 
 
 
使用Electron-Forge打包成桌面應用程式 
 
資料結構的宣告 文件結構 根據不同的做法,會有不同的文件結構
使用git submodule維護前端與後端共同結構參考 (推薦) 
使前端或後端其中一方,屬於對方的文件結構之下 (稍微不推薦) 
使用tsconfig.json的paths與references來維護前端與後端的共同結構參考 (不推薦) 
 
我自己就只先示範第一種
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 [獨立的共享宣告結構專案] shared-declare-types/   (git repository)     ├── ipc.ts     ├── ipc-valid-channel.ts     ├── ipc-routes.declare.ts     └── data.ts ========================================================= [主專案,引用共享宣告結構專案] project-root/   (git repository) ├── electron-src/ |   ├── declares/ |   |     ├── ipc-main-overwrite.declare.ts |   |     ├── app-router.ts │   |     └── electron-ipc-route.declare.ts │   ├── app.ts │   ├── ipc-routes.ts │   └── preload.ts │   └── shared-declare-types/   (git submodule -> shared-declare-types) │       ├── ipc.ts |       ├── ipc-routes.declare.ts │       ├── ipc-valid-channel.ts │       └── data.ts ├── angular-app/ │   ├── app/ |   ├── @services/ │   │   ├── ipc-renderer.service.ts │   │   └── startup_test.service.ts │   └── shared-declare-types/   (git submodule -> shared-declare-types) │       ├── ipc.ts │       ├── ipc-valid-channel.ts |       ├── ipc-routes.declare.ts │       └── data.ts ├── forge.config.ts ├── tsconfig.json └── package.json 
 
共同參考結構 首先我們需要先宣告一個結構,這個結構是假裝前後端的TS都需要共同參考的結構
Example: shared-declare-types/data.ts
1 2 3 4 5 6 export  interface  Data<T extends  string> {    key : string ;     value: string ;     timestamp: number ;     description?: T;  } 
 
接著需要定義IPC共用通道宣告
Example: shared-declare-types/ipc-valid-channel.ts
1 2 3 4 5 6 export  interface  IPCValidChannelMap {    startup_test : "startup_test" ; } export  type  IPCValidChannel = keyof IPCValidChannelMap;
 
定義Electron IPC接口的宣告 
 
Example: shared-declare-types/ipc.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  { IpcMain, IpcMainEvent, IpcMainInvokeEvent } from  "electron" ;export  type  IPCValidChannel<T> = T extends  string  ? T : never ;export  interface  IPCRenderer<T> {    send<R = never  | undefined >(channel: IPCValidChannel<T>): void ;     send<R extends  any []>(channel: IPCValidChannel<T>, ...args: R): void ;     receive<K>(channel: IPCValidChannel<T>, func: (args: K ) =>  void ): void ;     invoke<P extends  any [], R>(channel: IPCValidChannel<T>, ...args: P): Promise <R>;     removeAllListeners(channel: IPCValidChannel<T>): void ; } export  interface  IPCRendererReceiveMessageHandler {    (...args: any []): void ; } export  interface  IPCMessageHandler<T extends  IpcMainEvent = IpcMainEvent> {    (event: T, ...args: any []): void ; } export  type  Params<H, G extends  IpcMainEvent = IpcMainEvent> = H extends  (ev: G, ...args: infer P) => any  ? P : never ;
 
定義Electron IPC路由簽章 
 
Example: shared-declare-types/ipc-routes.declare.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import  {    IpcMainEvent,                IpcMainInvokeEvent       } from  "electron" ; import  { Data } from  "./shared-declare-types/data" ;export  interface  startup_test<T extends  IpcMainInvokeEvent> {    (_ev: T, ...args: any []): string ; } export  interface  ipc_message_wtih_args<T extends  IpcMainEvent, U extends  Data<string>> {    (_ev: T, desc : U): Promise <boolean >; } 
 
這些參考分別會被Electron的Preload與Angular的Service共同參考
 
Electron結構宣告 在Electron的主進程中宣告相關定義
定義Electron IPC 被複寫 
 
Example: electron-src/decalres/ipc-main-overwrite.declare.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 export  class  IPCMainOverwrite   {    private  _ipc: IpcMain;     constructor (ipc: IpcMain )  {         this ._ipc = ipc;     }     private  ipcSocket<T, R = IpcMainEvent>(channel: IPCValidChannel, callback : (channel: string , _ev: R, ...args: any [] ) =>  T, _ev : R, ...args: any []): T {         return  callback(channel, _ev, ...args);     }     public  handle<R>(channel: IPCValidChannel, callback : <T extends IpcMainInvokeEvent>(_ev: T, ...args: any[]) => R | Promise<R>): void {         this._ipc.handle(channel, callback);     }     public on<R>(channel: IPCValidChannel, callback: <T extends IpcMainEvent>(channel: string, _ev: T, ...args: any[]) => R): void {         this._ipc.on(channel, (event, ...args) => {             const result = this.ipcSocket<ReturnType<typeof callback>>(channel, callback, event, ...args);             Promise.resolve(result).then((result) => {                 event.reply(channel, result);             }).catch((error) => {                 console.log(error);             });         });     } } 
 
宣告IPC路由的簽章,並組合路由 
 
為每個 IPC 通道定義特定的輸入和輸出。
這為每個通道的功能創建了明確的宣告。
將所有路由簽章合併到一個主介面。此介面代表了 IPC 系統的整個 API 介面。
Example: electron-src/decalres/electron-ipc-route.declare.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  {    IpcMainEvent,                IpcMainInvokeEvent       } from  "electron" ; import  {    startup_test,     ipc_message_wtih_args } from  "electron-src/shared-declare-types/ipc-routes.declare"  import  { Data } from  "./shared-declare-types/data" ;export  interface  ElectronIpcMainRouter {    startup_test : startup_test<IpcMainInvokeEvent>;     ipc_message_wtih_args: ipc_message_wtih_args<IpcMainEvent, Data<string >>; } 
 
實作AppMainRouter 
 
使用 ElectronIpcMainRouter 介面來實作你的路由器。 這個路由器將會處理 IPC 的請求。
Example: electron-src/declares/app-router.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import  { ElectronIpcMainRouter } from  "electron-src/declares/electron-ipc-route.declare" ; export  class  AppRouter  implements  ElectronIpcMainRouter   {    public  static  create_router(): AppRouter {         return  new  AppRouter();     }     private  constructor ( )  {     }     startup_test(_ev: IpcMainInvokeEvent): string  {         return  "Pong" ;     }     ipc_message_wtih_args(_ev: IpcMainEvent, desc : Data<string >): Promise <boolean > {         return  Promise .resolve(true );     } } 
 
在Electron主程式中的宣告 
 
在 Electron 主行程檔案中,引用 AppRouter 並實作 IPC 路由。
並且使用 IPCMainOverwrite 來處理 IPC 的請求。
這樣可以確保你的 IPC 路由是類型安全的,並且符合你在 ElectronIpcMainRouter 中定義的介面。
TypeScript 會強制你的實作(函數體)與路由器介面的簽章相符。
Example: electron-src/ipc-routes.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import  { IpcMain, IpcMainEvent, IpcMainInvokeEvent } from  "electron" ;import  { IPCMainOverwrite } from  "electron-src/declares/ipc-main-overwrite.declare.ts" ; import  { AppRouter } from  "electron-src/declares/app-router" ; export  class  IPCRoute   {    private  ipc: IPCMainOverwrite;     private  router: AppRouter;     public  static  create_ipc_router(ipc: IpcMain): IPCRoute {         const  router = AppRouter.create_router();         return  new  IPCRoute(ipc, router);     }     private  constructor (_parent_ipc: IpcMain, router: AppRouter )  {         this .ipc = new  IPCMainOverwrite(_parent_ipc);         this .router = router;                  this .ipc.handle("startup_test" , router.startup_test);     }     public  handle(channel: IPCValidChannel, callback : <T extends IpcMainInvokeEvent>(_ev: T, ...args: any[]) => any): void {         this.ipc.handle(channel, callback);     }     public on(channel: IPCValidChannel, callback: <T extends IpcMainEvent>(channel: string, _ev: T, ...args: any[]) => any): void {         this.ipc.on(channel, callback);     } } 
 
在Electron Entry Point中使用IPCRoute 
 
Example: electron-src/app.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  {    app,     BrowserWindow,     ipcMain,     Menu,     dialog,     nativeImage,     NativeImage,     screen } from  "electron" ; import  {    IPCRoute } from  "./ipc-routes" ; const  ipc_route = IPCRoute.create_ipc_router(ipcMain);
 
在Electron主程式中使用Preload腳本 
 
Example: electron-src/preload.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import  { contextBridge, ipcRenderer } from  "electron" ;import  {    IPCRenderer } from  "electron-src/shared-declare-types/ipc" ; import  {    IPCValidChannel } from  "electron-src/shared-declare-types/ipc-valid-channel" ; ipcRenderer.setMaxListeners(Infinity ); const  valid_IPC_channels: Array <IPCValidChannel> = [    "startup_test"  ]; contextBridge.exposeInMainWorld(     "ipcRenderer" ,     <IPCRenderer<IPCValidChannel>>{         send : <R extends any[]>(channel: IPCValidChannel, ...args: R): void => {             if (valid_IPC_channels.includes(channel)) {                 ipcRenderer.send(channel, ...args);             } else {                 console.error(`[ipcRenderer.send]Invalid IPC channel: ${channel}`);             }         },         // eslint-disable-next-line @typescript-eslint/ban-types         receive: <U>(channel: IPCValidChannel, func: (args: U) => void): void => {             if (valid_IPC_channels.includes(channel)) {                 ipcRenderer.on(channel, (event, args: U) => func(args));             } else {                 console.error(`[ipcRenderer.receive]Invalid IPC channel: ${channel}`);             }         },         // eslint-disable-next-line arrow-body-style         invoke: <P extends any[], R>(channel: IPCValidChannel, ...args: P): Promise<R> => {             if (valid_IPC_channels.includes(channel)) {                 return ipcRenderer.invoke(channel, ...args) as Promise<R>;             }             console.error(`[ipcRenderer.invoke]Invalid IPC channel: ${channel}`);             return Promise.reject(new Error(`[ipcRenderer.invoke]Invalid IPC channel: ${channel}`));         },         removeAllListeners: (channel: IPCValidChannel): void => {             if (valid_IPC_channels.includes(channel)) {                 ipcRenderer.removeAllListeners(channel);             } else {                 console.error(`[ipcRenderer.removeAllListeners]Invalid IPC channel: ${channel}`);             }         }     } ); 
 
Angular結構宣告 接著輪到Angular的部分,會簡單的多
將Electron的IPC Renderer封裝成Angular Service即可
Angular IpcRenderer Service 
 
Example: angular-app/app/@services/ipc-renderer.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import  { Injectable, OnInit, Inject, Optional } from  "@angular/core" ;import  { switchMap, distinctUntilChanged, catchError, map } from  "rxjs/operators" ;import  { forkJoin, of , takeUntil, ReplaySubject, Observable, lastValueFrom, from , mergeMap, share  } from  "rxjs" ;import  { IPCRenderer, Params } from  "angular-app/app/shared-declare-types/ipc" ;import  { IPCValidChannel } from  "angular-app/app/shared-declare-types/ipc-valid-channel" ;declare  global  {    interface  Window {         ipcRenderer : IPCRenderer<IPCValidChannel>;     } } @Injectable ({    providedIn : "root"  }) export  class  IPCRendererService   {    private  _destroyed$: ReplaySubject<boolean > = new  ReplaySubject(1 );     private  readonly  receive_map: Map <IPCValidChannel, Observable<any >> = new  Map <IPCValidChannel, Observable<any >>();     constructor ( )  {         console .info("[IPCRendererService.constructor]IPCRendererService constructed." );     }     public  send<T=never |undefined >(channel: IPCValidChannel): void ;     public  send<T extends  any []>(channel: IPCValidChannel, ...data: T): void ;     public  send<T extends  any []>(...args: T): void  {                  if  (window .ipcRenderer && window .ipcRenderer.send) {             if  (args.length === 1 ) {                                  window .ipcRenderer.send<T>(args[0 ]);             } else  if  (args.length > 1 ) {                                  window .ipcRenderer.send<Array <any >>(args[0 ], ...args.slice(1 ));             } else  {                 console .error(`[IPCRendererService.send]Invalid IPC channel: ${args[0 ]} ` );             }         } else  {             console .error(`[IPCRendererService.send]Invalid IPC channel: ${args[0 ]} ` );         }     }     public  invoke<P extends  any [], R>(channel: IPCValidChannel, ...args: P): Promise <R> {         return  new  Promise ((resolve, reject ) =>  {                          if  (window .ipcRenderer && window .ipcRenderer.invoke) {                                  window .ipcRenderer.invoke<P, R>(channel, ...args).then((data: R ) =>  {                     resolve(data);                 }).catch((error: Error  ) =>  {                     reject(error);                 });             } else  {                 console .error(`[IPCRendererService.invoke]Invalid IPC channel: ${channel} ` );                 return  reject(new  Error (`[IPCRendererService.invoke]Invalid IPC channel: ${channel} ` ));             }         });     }     public  receive<T>(channel: IPCValidChannel): Observable<T> {         if  (this .receive_map.has(channel)) {             return  this .receive_map.get(channel) as  Observable<T>;         }         const  observable = (new  Observable((subscriber ) =>  {                          if  (window .ipcRenderer && window .ipcRenderer.receive) {                                  window .ipcRenderer.receive<T>(channel, (data ) =>  {                     subscriber.next(data);                 });             } else  {                 console .error(`[IPCRendererService.receive]Invalid IPC channel: ${channel} ` );                 subscriber.error(new  Error (`[IPCRendererService.receive]Invalid IPC channel: ${channel} ` ));             }         })).pipe<T>(share<any >());         this .receive_map.set(channel, observable);         observable.subscribe((data ) =>  {         });         return  observable;     } } 
 
可能有的人會發現,為什麼並沒有自動推論型別,這是因為需要在範型中加入參數,進行推論,比如你封裝startup_test這個通道
封裝startup_test通道 
 
Example: angular-app/app/@services/startup_test.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import  { Injectable, OnInit, Inject, Optional } from  "@angular/core" ;import  { switchMap, distinctUntilChanged, catchError, map } from  "rxjs/operators" ;import  { forkJoin, of , takeUntil, ReplaySubject, Observable, lastValueFrom, from , mergeMap, share  } from  "rxjs" ;import  { IPCRenderer, Params } from  "angular-app/app/shared-declare-types/ipc.model" ;import  { IPCValidChannel } from  "angular-app/app/shared-declare-types/ipc-valid-channel" ;import  { IPCRendererService } from  "./ipc-renderer.service" ;import  {    startup_test } from  "angular-app/shared-declare-types/ipc-routes.declare" ; @Injectable ({    providedIn : "root"  }) export  class  SomeService   {    private  _destroyed$: ReplaySubject<boolean > = new  ReplaySubject(1 );     constructor (private  ipcRenderer: IPCRendererService )  {     }     public  ping ( )  {         return  from (this .ipcRenderer.invoke<Params<startup_test<any >>, ReturnType<startup_test<any >>>("startup_test" ));     } }