Open5
Electronで超大規模なアプリケーションを作る方法
超大規模なアプリケーションとは
想定しているのは、様々な機能を提供し、マシンパワーを必要とするようなタスクを担い、複数のウインドウから構成され、さまざまなコンテンツを表示するアプリケーションを想定している。
なぜElectronなのか
クロスプラットフォームである
- OSを問わずアプリケーションを作ることができる。
- CPUのアーキテクチャを問わない。
並列処理が可能である
- メインプロセスでは
worker_threads
モジュールの Worker、レンダラープロセスではWeb Workerを用いてマルチスレッドプログラミングができる。 - オフスクリーンレンダリング により画像の描画も並列化できる。
-
child_process
モジュールによるマルチプロセス環境も使える。
速度が必要な処理を実装できる
- WebAssembly を用いた高速な計算ができる
- native modulesも使える(Electron向けにリビルドする必要があり手間がかかる)
JavaScript向けの様々なツールが揃っている
- TypeScriptなどの静的型付け言語
- ReactなどのUIフレームワーク・ライブラリ
- Reduxなどのデータ管理ライブラリ
- Chakra-UIなどのコンポーネント群
- D3.js などのデータ操作・可視化用ライブラリ
- GraphQLを用いたデータ取得
- ESLintやSylelintといった静的解析ライブラリ
- Jestなどのテストフレームワーク
- Prettierによるコード生成
- sqlite3やSQL.jsといったデータベース
- Webpackなどのモジュールバンドラ
- モノレポ環境
モノレポ環境を考えてみる
超大規模の場合はモノレポ環境が必須だと思う(「思う」としたのは、試したことがないためである)。
JavaScriptについて
色々あるので好きなのを選ぶといいと思う(試したことがない)。
レンダラープロセスは、ウインドウごとにパッケージを分割するといいんじゃないか。
それから、ロジックと画面を分けるのもいいと思う。
試したことがないからわからない。
Rust(WebAssembly)について
高速な計算が必要なケースについてはRustで書いたWebAssemblyを使う気満々なので、Rustを使う場合について考える。
JavaScript のモノレポのディレクトリに合わせてクレートを配置するといいと思う。
イメージ
apps/
app/ - ここがElectronを起動する起点
package.json
main.js
main-feature1/
Cargo.toml
package.json
main-feature2/
renderer-feature3/
renderer-feature4/
lib-feature5/
GraphQLを用いた堅牢なIPC通信
以前この記事を書いた。
同じようなことを考える人はいるもので、この記事を書いた後に以下のリポジトリを見つけた。
ApolloをベースにしているのでSubscriptionも使えるそうだ。便利そう(試してない)。
とはいえ、車輪の再発明は勉強になるので、自分なりの方法を提案する。
コード生成
typescript-generic-sdk
が非常に便利だと知った。
typescript-generic-sdk
を使うと下記のようなコードが出力される。
// 一部抜粋
export type Requester<C= {}> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R>
export function getSdk<C>(requester: Requester<C>) {
return {
createTeam(variables: CreateTeamMutationVariables, options?: C): Promise<CreateTeamMutation> {
return requester<CreateTeamMutation, CreateTeamMutationVariables>(CreateTeamDocument, variables, options);
},
getTeam(variables: GetTeamQueryVariables, options?: C): Promise<GetTeamQuery> {
return requester<GetTeamQuery, GetTeamQueryVariables>(GetTeamDocument, variables, options);
},
};
}
export type Sdk = ReturnType<typeof getSdk>;
すると、以下のように書くことで、IPC通信にGraphQLを使える。
// renderer process
declare global {
interface Window {
electron: {
// preload.jsで設定してください
graphql(a: string, b: any): Promise<ExecutionResult>
}
}
}
const client = getSdk(async (doc, vars) => {
const result = await window.electron.graphql(doc, vars)
if (result.errors) {
// 真面目に書こう
}
return result.data as any
})
async function main() {
const response = await client.getTeam({ teamId: 42 })
console.log(response.team)
}
スレッド間通信で送信するデータにしっかり型をつける
Redux Toolkitの createAction
が便利だった。
import { createAction } from '@reduxjs/tooklit'
const increment = createAction<number, 'increment'>('increment')
const decrement = createAction<number, 'decrement'>('decrement')
type Action = ReturnType<typeof increment | typeof decrement>
const worker = new Worker()
worker.addEventListener('message', e => {
const action = e.data as Action
switch (e.type) {
case increment.type:
console.log(`increment: ${e.payload}`)
break
case derement.type:
console.log(`decrement: ${e.payload}`)
break
}
})