Electron + React & Python Flask でパッケージ化アプリを作ってみた

背景


Python で動くバックエンドと連動する GUI アプリケーションをユーザーに配布するため、Electron によるデスクトップアプリケーションを作ってみる.

バックエンド


バックエンドは、pipenv を使用して仮想の Python 環境を作成し、その後、pyinstaller を使用してバックエンド全体をパッケージ化して、フロントエンドの Electron アプリが使用できるようにする。

pip install pipenv
pipenv install flask
pipenv install pip run pyinstaller
mkdir be
cd be

main.py を作成し、Python Flask を使用してバックエンド API を実装する。

from flask import Flask, request, jsonify
from flask_caching import Cache
from flask_cors import CORS
from waitress import serve

app = Flask(__name__)
cache = Cache(app, config={"CACHE_TYPE": "simple"})
CORS(app)

if getattr(sys, "frozen", False):
    child_dir = os.path.dirname(sys.executable)
    base_dir = os.path.dirname(child_dir)
else:
    base_dir = os.path.dirname(os.path.abspath(__file__))

@app.route("/api_1", methods=["POST"])
def api_1():
    body = request.json
    print(body)
		info = {}
		info["id"] = 1234
		cache.set("info", info)
    return jsonify({
        "message": "test api_1",
        "flag": True,
    })

@app.route("/api_2", methods=["POST"])
def api_2():
		driving_info = cache.get("info")
		print(driving_info)
    return jsonify({
		    "message": "test api_1",
		    "flag": True,
    })

if __name__ == "__main__":
	  # app.run(host="127.0.0.1", port=5000)
    serve(app, host="127.0.0.1", port=5000)

/api_1/api_2 の二つのエンドポイントがあり、それぞれ POST リクエストを受け取る。 /api_1 は json データを受け取り、それを処理して json レスポンスを返す。 /api_2/api_1 で flash-caching で保存された情報を取得し、json レスポンスを返す。flask-cors を使用して、クロスドメインアクセスを許可する。

また、production モードでは、次の警告が出るので、serve を導入しないといけない。

WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.

最後は、main.py を pyinstaller でパッケージ化して、アプリケーションとして Electron 側から使用することができる。アプリの位置は be/dist となる。

pipenv run pyinstaller -F main.py

フロントエンド


後続の調整があるため、フロントエンドのユーザーインターフェースは、Python の GUI ライブラリより柔軟性のある React を選択する。

まず、新しい React プログラムを作成する。

npx create-react-app my-project --typescript

App.tsx に、バックエンドの Flask アプリのデフォルトポート 5000 に対し、試しに FETCH を作成する。

export const BASE_API_URL = "http://127.0.0.1:5000";

function App() {
	const fetchApi = () => {
		try {
      await fetch(`${BASE_API_URL}/api_1`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
				body: JSON.stringify(formInfo),
			}).then((res: Response) =>
		...
	}
}

Electron の設定


npm で electron 及び関連のパッケージをインストール。

npm i -D electron
npm i -D electron-builder
npm i electron-is-dev

main.js に Electron アプリの設定をする

const { app, BrowserWindow } = require("electron");
const path = require("path");
const isDev = require("electron-is-dev");
const { exec, execFile } = require("child_process");

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  if (isDev) {
    mainWindow.loadURL("http://localhost:3000/");
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile("./build/index.html");
  }
}

app.whenReady().then(() => {
  createWindow();
  app.on("activate", function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
  const backend = path.join(process.cwd(), "be/dist/main.exe");

  execFile(
    backend,
    {
      windowsHide: true,
    },
    (err, stdout, stderr) => {
      if (err) {
        console.log(err);
      }
      if (stdout) {
        console.log(stdout);
      }
      if (stderr) {
        console.log(stderr);
      }
    }
  );
});

app.on("window-all-closed", function () {
  exec(`taskkill /f /t /im main.exe`, (err, stdout, stderr) => {
    if (err) {
      console.log(err);
      return;
    }
    if (stderr) {
      console.log(`stderr: ${stderr}`);
    }
  });
  if (process.platform !== "darwin") app.quit();
});

build.js に Electron ビルドの設定をする。

const path = require("path");
const builder = require("electron-builder");

builder
  .build({
    projectDir: path.resolve(__dirname),
    win: ["portable", "nsis"],
    config: {
      appId: "React Based Electron",
      productName: "React Based Electron",
      copyright: "Copyright © 2023 MKSC",
      directories: {
        output: "electron-build/win",
      },
      win: {
        icon: path.resolve(__dirname, "logo512.png"),
      },
      files: [
        "build/**/*",
        "node_modules/**/*",
        "package.json",
        "main.js",
        "preload.js",
      ],
      extraFiles: [
        {
          from: "be",
          to: "be",
          filter: ["**/*"],
        },
      ],
      extends: null,
    },
  })
  .then(
    (data) => console.log(data),
    (err) => console.error(err)
  );

win は、Windows向けのインストーラーを作成するための electron-builder のオプションです。 portable で作成されたプログラムはインストール不要で使用できますが、起動に時間がかかります。一方、 nsis で作成されたプログラムはインストールが必要ですが、起動時間は短くなる。

最後は、package.json で関連するコマンドを設定することで完成。

npm run electron-build を実行することで、作成されたアプリケーションは ./eletron-build の配下にある。

"scripts": {
    "start": "set BROWSER=none&& react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "wait-on http://127.0.0.1:3000 && electron .",
    "electron-build": "npm run build && node build.js",
    "format": "prettier --write \"./**/*.{tsx,ts,js}\""
  },

バックエンドをまずパッケージ化してから、Electron 側のフロントエンドからバックエンドをパッケージ化する方法を採用しているため、追加ファイルのパスについては別途工夫が必要。

懸念点としては、development モードでバックエンドアプリを停止する際、CORS の原因で FETCH がブロックされるので、Electron の方も再起動しないといけない。

この記事をシェア

弊社では、一緒に会社を面白くしてくれる仲間を募集しています。
お気軽にお問い合わせください!