IPCを使ってJSONファイルを読み込んで描画する
ElectronではIPCを使ってメインプロセスに処理を任せることで、Webブラウザだけではできないことを実現します。任意のファイルを読み書きしたり、Node.jsの豊富なnpmパッケージを利用したり、さまざまなネイティブモジュールを使ったりすることも可能です。
今度は、Elecronが提供するOSネイティブの、ファイルオープンダイアログを使ってJSONファイルを読み込み、Productivity
コンポーネントで描画してみましょう。
global.d.tsを書き換える
// src/@types/global.d.ts interface Window { //(1)対象はWindowというインターフェース ipc: { openFile: () => Promise<{ filename: string; content: string } | null>; //(2)openFile関数を定義 }; }
前編で軽く解説しましたが、改めて解説します。d.ts
というのは、TypeScriptで型宣言のみを行うファイルです。
(1)で通常、Webブラウザ上でプログラミングしているときに使うwindow
グローバルオブジェクトにipc
というオブジェクトを追加しています。
(2)でopenFile
という関数を定義します。関数の引数はなしでPromiseを返すものです。前編でも使ったPromise
は非同期処理を行うための便利な仕組みです。
Promise<{ filename: string; content: string } | null>;
の型アノテーションは、非同期処理が完了した際に{ filename: string; content: string }
というオブジェクトまたはnull
を引き渡してくれる型を意味します。
window.ipc.openFile().then((result) => { if (result) { // nullではないためオブジェクト console.log(result.filename); console.log(result.content); } else { console.log("result is NULL!"); } });
このように使います。
preload.jsを書き換える
次に、public/preload.js
を書き換えます。
// public/preload.js const { contextBridge, ipcRenderer, remote } = require("electron"); //(1)contextBridgeを使って、メインプロセスの機能をレンダリングプロセスに提供する contextBridge.exposeInMainWorld("ipc", { openFile: () => { return ipcRenderer.invoke("openFile"); }, });
前編とは異なり、先ほど型定義をした戻り値Promise<{ filename: string; content: string } | null>
があるため、ipcRenderer.invoke("openFile")
の戻り値をそのままreturn
しています。
electron.jsを書き換える
public/electron.js
の末尾にopenFile
の処理を追加します。
//(1)invokeできるopenFileを定義する ipcMain.handle("openFile", (event) => { //(2)処理の中身が非同期なのでasync関数を定義 const read = async () => { //(3)dialog.showOpenDialogでファイルオープンダイアログを開く const { canceled, filePaths } = await dialog.showOpenDialog(null, { properties: ["openFile"], title: "JSONファイルを開く", defaultPath: ".", filters: [{ name: "JSON file", extensions: ["json"] }], }); //(4)canceledなら、nullを返す if (canceled) { return null; } //(5)ダイアログで指定されたファイルをUTF-8とみなして読み込む const content = await fs.promises.readFile(filePaths[0], { encoding: "utf-8", }); //(6)filenameとcontentのオブジェクトを返す return { filename: filePaths[0], content }; }; //(7)定義されたasync関数を実行する return read(); });
(1)openFile
という名前でinvokeできる定義をしています。
(2)は前編でも登場したasync
関数で、ダイアログの処理とファイル読み込みという、2つの非同期処理を束ねるのに便利なため、関数を宣言しています。async
関数の戻り値はPromise
になります。
(3)ではdialog.showOpenDialog
関数で、OSネイティブのファイルオープンダイアログを開いています。
await
は前編の記事でも説明した通り、async
関数の中でのみ使えるキーワードで、await
の後ろに指定したPromise
の完了を待つものです。
dialog.showOpenDialog
関数の戻り値はPromise
であり、普通は処理が非同期で行われますが、await
により、処理が完了するまで待つという、同期処理と同様の振る舞いになります。Promise
のthen
メソッドを並べるメソッドチェーンをしなくて済むこと、普通のプログラミングと同じように記述できてわかりやすいなどのメリットがあります。
さて、関数に設定しているオプションの説明をします。
properties
配列にmultifile
を追加すれば複数のファイルを開くこともできます。
defaultPath
は、ダイアログの初期のパスです。指定しなければOSが自動で設定し、ホームディレクトリか、前回開いたディレクトリなどになるはずです。今回はカレントディレクトリである.
を指定しています。
filters
は、今回はJSONファイルなので、拡張子がjson
のものだけ開くために設定しています。
(4)では、ダイアログの結果を見てcanceled
の場合null
を返しています。
(5)ではfilePaths[0]
に、ダイアログで指定されたファイル名が、フルパスで入っているため、Node.jsのfs.promises.readFile
でファイル読み込みをします。第2引数に{encoding: "utf-8"}
を指定しているのはreadFile
が返してくるデータをUTF-8文字列に固定するためです。もし別の文字コードが入る可能性があればencoding
を指定せず文字コード判定をする必要があるでしょう。
(6)では、filename
と読み込んだcontent
のオブジェクトを返しています。
(7)では、(2)で定義したasync
関数を呼び出しています。async
関数の呼び出しなので、戻り値はPromise
です。
ここまでで、global.d.ts
の型定義と、preadload.js
で、メインプロセスへのinvoke
と、electron.js
でメインプロセス上で実行されるファイルオープンダイアログおよびファイル読み込み処理の実装を行いました。
これでやっとレンダラープロセスから、window.ipc.openFile
を呼び出すことができるようになりました。
App.tsxを書き換える
先ほど、createData
関数で固定のサンプルデータを生成していたApp.tsx
を、openFile
によって読み込んだJSONファイルのデータを描画するように書き換えます。
// src/App.tsx import React, { useState, useCallback } from "react"; import Productivity from "./Productivity"; import { Data } from "./types"; import "@grapecity/wijmo.styles/wijmo.css"; import "@grapecity/wijmo.cultures/wijmo.culture.ja"; const App: React.FC = () => { //(1)filename, dataをステート定義 const [filename, setFilename] = useState("not opend"); const [data, setData] = useState<Data[]>([]); //(2)ダイアログを開く、ボタンのコールバックを定義 const handleDialog = useCallback(() => { //(2-1)window.ipc.openFile関数を呼び出して Promise の処理 window.ipc.openFile().then((res) => { //(2-2)戻り値がnullでなければ if (res) { // (2-3)setFilenameとsetDataでステートを更新する setFilename(res.filename); setData(JSON.parse(res.content)); } }); }, []); return ( <> <div> <span>{filename}</span> <button onClick={handleDialog}>JSON File Open</button> </div> {data.length > 0 && <Productivity data={data} />} </> ); }; export default App;
(1)ではReactHooksのuseState
関数でステートを定義しています。filename
は文字列型でファイル名、data
はData
配列型です。useState
の引数がステートの初期値です。
(2)ではダイアログを開いてファイルを読み込み、ステート更新を行うためのコールバックを定義しています。
(2-1)でwindow.ipc.openFile
関数を呼び出してPromise
の処理のためにthen
メソッドを記述しています。
(2-2)で戻り値がnullではないことを確認しています。
(2-3)で戻り値からfilename
を取り出してsetFilename
でファイル名のステートを更新し、content
を取り出してJSON.parse
でJSONをデータ化してからsetData
で、データのステートを更新しています。
これらのステート更新が行われると、再描画が行われて<Productivity data={data}>
が更新されます。
JSONファイルを用意して実際に動かす
では、JOSNファイルを使って実際に動かしてみます。
まずは2018.json
というファイルに2018年のデータを入れます。
[ { "ym": "2018/09", "time": 160, "count": 200 }, { "ym": "2018/10", "time": 200, "count": 300 }, { "ym": "2018/11", "time": 120, "count": 150 }, { "ym": "2018/12", "time": 240, "count": 200 } ]
次に、2020.json
というファイルに2020年のデータを入れましょう。
[ { "ym": "2020/09", "time": 140, "count": 300 }, { "ym": "2020/10", "time": 180, "count": 400 }, { "ym": "2020/11", "time": 100, "count": 250 }, { "ym": "2020/12", "time": 200, "count": 300 } ]
この状態で実際に動かしてみましょう。electron.js
などのメインプロセスは書き換えても自動では反映されないため、一度Electronアプリを再起動する必要があります。
Error occurred in handler for 'openFile': No handler registered for 'openFile'
再起動を忘れると、このようにopenFile
のハンドラーが登録されていないエラーが出るはずです。
再起動して立ち上げ直すと以下の画面になります。
JSONファイルを選ぶとそれに合わせて更新されます。