LoginSignup
9
19

More than 3 years have passed since last update.

Electronをcreate-react-app + TypeScriptでやるしfsも使う

Last updated at Posted at 2020-08-13

create-react-appとElectronを併用したとき、
fsなどのnodeでしか動かないパッケージを使う方法がわかりにくかったので記事に残しておく。

リポジトリはこちら

概要

この記事でやること

  • create-react-app (CRA) を使う
  • TypeScriptでコードを書く
  • Electronでreactのページを開く
  • アプリ中でfsを使う(nodeIntegration: false のままで)
  • アプリ中でデータを保存できるようにする
  • デスクトップアプリ (.exe) にする

やらないこと

  • エディタやnpmなどの用意
  • linter類の設定
  • アプリの実装
  • テストの作成

手順

  1. CRAでプロジェクトを作る
  2. Electronを使えるようにする
  3. アプリ中でfs・保存を使えるようにする
  4. 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を変更する

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
electron/main.ts
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.

electron/tsconfig.json
{
  "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のファイルを編集する

electron/main.ts
// ...
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
electron/ipc-main-handler.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}`);
  });
};

electron/preload.ts
import { ipcRenderer, contextBridge } from "electron";

contextBridge.exposeInMainWorld("myAPI", {
  readDir: () => ipcRenderer.invoke("read-dir"),
  save: (str: string) => ipcRenderer.invoke("save", str),
});

型定義ファイルを追加する

@types/global.d.ts
declare global {
  interface Window {
    myAPI: Sandbox;
  }
}

export interface Sandbox {
  readDir: () => Promise<string[]>;
}

tsconfig.jsonを変更する

tsconfig.json
{
  "compilerOptions": {
    ...
  }
  "files": [
    "@types/global.d.ts"
  ],
  "include": [
    "src"
  ]
}

Reactアプリを変更し、fsの使用・保存ができることを確認できるようにする

src/App.tsx
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を更新する

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を編集し、出力がリポジトリに残らないようにする

.gitignore
# ...

# electron output
/dist

参考

create-react-appとelectron-builderでTypeScriptとHot Reloadに完全対応したElectronアプリ開発環境を作成する

Electronのセキュリティについて大きく誤認していたこと


Electronで設定ファイルを導入

9
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
19