Open5

Electronで超大規模なアプリケーションを作る方法

kaorun343kaorun343

超大規模なアプリケーションとは

想定しているのは、様々な機能を提供し、マシンパワーを必要とするようなタスクを担い、複数のウインドウから構成され、さまざまなコンテンツを表示するアプリケーションを想定している。

kaorun343kaorun343

なぜ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などのモジュールバンドラ
  • モノレポ環境
kaorun343kaorun343

モノレポ環境を考えてみる

超大規模の場合はモノレポ環境が必須だと思う(「思う」としたのは、試したことがないためである)。

JavaScriptについて

色々あるので好きなのを選ぶといいと思う(試したことがない)。
レンダラープロセスは、ウインドウごとにパッケージを分割するといいんじゃないか。
それから、ロジックと画面を分けるのもいいと思う。
試したことがないからわからない。

Rust(WebAssembly)について

高速な計算が必要なケースについてはRustで書いたWebAssemblyを使う気満々なので、Rustを使う場合について考える。

JavaScript のモノレポのディレクトリに合わせてクレートを配置するといいと思う。

https://doc.rust-lang.org/cargo/reference/workspaces.html#the-workspace-section

イメージ

apps/
  app/ - ここがElectronを起動する起点
    package.json
    main.js
  main-feature1/
    Cargo.toml
    package.json
  main-feature2/
  renderer-feature3/
  renderer-feature4/
  lib-feature5/
kaorun343kaorun343

GraphQLを用いた堅牢なIPC通信

以前この記事を書いた。

https://zenn.dev/kaorun343/articles/654a9673863388

同じようなことを考える人はいるもので、この記事を書いた後に以下のリポジトリを見つけた。

https://github.com/fubhy/graphql-transport-electron

ApolloをベースにしているのでSubscriptionも使えるそうだ。便利そう(試してない)。

とはいえ、車輪の再発明は勉強になるので、自分なりの方法を提案する。

コード生成

typescript-generic-sdk が非常に便利だと知った。

https://www.graphql-code-generator.com/docs/plugins/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)
}
kaorun343kaorun343

スレッド間通信で送信するデータにしっかり型をつける

Redux Toolkitの createAction が便利だった。

https://redux-toolkit.js.org/api/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
  }
})