create-react-appとElectronを併用したとき、
fsなどのnodeでしか動かないパッケージを使う方法がわかりにくかったので記事に残しておく。
リポジトリはこちら
概要
この記事でやること
- create-react-app (CRA) を使う
- TypeScriptでコードを書く
- Electronでreactのページを開く
- アプリ中でfsを使う(nodeIntegration: false のままで)
- アプリ中でデータを保存できるようにする
- デスクトップアプリ (.exe) にする
やらないこと
- エディタやnpmなどの用意
- linter類の設定
- アプリの実装
- テストの作成
手順
- CRAでプロジェクトを作る
- Electronを使えるようにする
- アプリ中でfs・保存を使えるようにする
- Electron-Builderで実行ファイルを作れるようにする
1. CRAでプロジェクトを作る
このセクションではCRAでプロジェクトのひな型を作る。
このセクションが終わった後npm start
を実行すれば、ブラウザでページが開く。
npx create-react-app cra-ts-electron --use-npm --template=typescript
cd cra-ts-electron
npm update
2. Electronを使えるようにする
このセクションではElectronでページが開くようにする。
このセクションが終わった後npm start
を実行すれば、ブラウザでなくElectronでページが開く。
必要なパッケージをインストールする
npm add electron electron-is-dev electron-reload electron-store typescript@latest
npm add -D concurrently cross-env npm-run-all rimraf wait-on
package.jsonを変更する
{
...
"homepage": "./",
"main": "build/electron/main.js",
"scripts": {
"test": "react-scripts test",
"eject": "react-scripts eject",
"postinstall": "electron-builder install-app-deps",
"wait-start": "wait-on http://localhost:3000",
"start": "concurrently \"npm run start:start-server\" \"npm run start:watch-electron\" \"npm run start:use-electron\"",
"start:start-server": "cross-env BROWSER=none react-scripts start",
"start:watch-electron": "run-s wait-start start:watch-electron:watch",
"start:watch-electron:watch": "tsc -p electron -w",
"start:use-electron": "run-s wait-start start:use-electron:build start:use-electron:run",
"start:use-electron:build": "tsc -p electron",
"start:use-electron:run": "electron ."
},
...
}
Electron用コードを追加する
以下のファイルを追加する。
electron/main.ts
electron/tsconfig.json
import { app, BrowserWindow } from "electron";
import * as path from "path";
import * as isDev from "electron-is-dev";
function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
},
});
if (isDev) {
win.loadURL("http://localhost:3000/index.html");
} else {
// 'build/index.html'
win.loadURL(`file://${__dirname}/../index.html`);
}
// Hot Reloading
if (isDev) {
// 'node_modules/.bin/electronPath'
require("electron-reload")(__dirname, {
electron: path.join(
__dirname,
"..",
"..",
"node_modules",
".bin",
"electron"
),
forceHardReset: true,
hardResetMethod: "exit",
});
}
// Open the DevTools.
if (isDev) {
win.webContents.openDevTools();
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": [
"dom",
"esnext"
],
"sourceMap": true,
"strict": true,
"outDir": "../build",
"rootDir": "../",
"noEmitOnError": false,
"typeRoots": [
"node_modules/@types"
],
"skipLibCheck": true
}
}
3. アプリ中でfs・保存を使えるようにする
このセクションではアプリ中で(間接的に)fsを使えるようにする。
またelectron-store
を使って設定ファイルなどを保存できるようにする。
このセクションが終わった後npm start
を実行すれば、Electronで開かれたページにカレントディレクトリにあるファイル・ディレクトリが表示される。
またC:\Users\<user-name>\AppData\Roaming\cra-ts-electron\config.json
に以下の内容が保存されている。
{
"unicorn": "uni-uni"
}
Electronのファイルを編集する
// ...
import { initIpcMain } from "./ipc-main-handler";
function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
},
});
// ...
}
// ...
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
initIpcMain();
Electronのファイルを追加する
以下のファイルを追加する。
electron/ipc-main-handler.ts
electron/preload.ts
@types/global.d.ts
import { ipcMain } from "electron";
import * as Store from "electron-store";
import * as fs from "fs";
export const initIpcMain = (): void => {
const store = new Store();
ipcMain.handle("read-dir", async () => fs.promises.readdir("./"));
ipcMain.handle("save", (event, str: string) => {
store.set("unicorn", str);
console.log(`save: ${str}`);
});
};
import { ipcRenderer, contextBridge } from "electron";
contextBridge.exposeInMainWorld("myAPI", {
readDir: () => ipcRenderer.invoke("read-dir"),
save: (str: string) => ipcRenderer.invoke("save", str),
});
型定義ファイルを追加する
declare global {
interface Window {
myAPI: Sandbox;
}
}
export interface Sandbox {
readDir: () => Promise<string[]>;
}
tsconfig.jsonを変更する
{
"compilerOptions": {
...
}
"files": [
"@types/global.d.ts"
],
"include": [
"src"
]
}
Reactアプリを変更し、fsの使用・保存ができることを確認できるようにする
import React, { useState, useEffect } from "react";
import logo from './logo.svg';
import './App.css';
const { myAPI } = window;
function App() {
const [text, setText] = useState("not loaded");
useEffect(() => {
const f = async () => {
setText("loading...");
try {
const dirs = await myAPI.readDir();
myAPI.save("uni-uni");
setText(`files are: ${dirs.join(", ")}`);
} catch (e) {
setText("loading was failed");
alert(e);
}
};
f();
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<p>
{text}
</p>
</header>
</div>
);
}
export default App;
4. Electron-Builderで実行ファイルを作れるようにする
このセクションではElectron-Builderで実行ファイルを作れるようにする。
このセクションが終わった後npm run build
を実行すれば、dist/cra-ts-electron 0.1.0.exe
が作成される。
electron-builderをインストールする
npm add -D electron-builder
package.jsonのdependenciesをdevDependenciesに移す
package.jsonのdependenciesの内、
以下のパッケージ以外をdevDependenciesに移す。
- electron-is-dev
- electron-store
- react
- react-dom
package.jsonを更新する
{
...
"author": "Sankaku",
"description": "Example of cra with electron",
"build": {
"extends": null,
"files": [
"build/**/*"
],
"directories": {
"buildResources": "assets"
},
"win": {
"target": "portable"
}
},
"scripts": {
...
"build": "run-s build:clean build:react build:electron build:electron-builder",
"build:clean": "rimraf build dist",
"build:react": "react-scripts build",
"build:electron": "tsc -p electron",
"build:electron-builder": "electron-builder"
},
...
}
.gitignoreを編集し、出力がリポジトリに残らないようにする
# ...
# electron output
/dist
参考
create-react-appとelectron-builderでTypeScriptとHot Reloadに完全対応したElectronアプリ開発環境を作成する
Electronのセキュリティについて大きく誤認していたこと
Electronで設定ファイルを導入