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ファイルを選ぶとそれに合わせて更新されます。

