概要
Flask+React+pywebview+PyInstallerでデスクトップアプリを作成し、動作を確認した。
背景と目的
ある目的でFlaskとReactで簡単なWebアプリを作成してローカルで動作させていたのだが、
デスクトップアプリとして1つにまとめたい状況
が発生した。そこで、実現するための方法について整理してメモしておく。
詳細
0. 環境
- Windows
- Python3.12
- node v20.18
1. 作成するもの、実現手段
以下の構成とする。
- reactでフロントエンドを作成
- ボタンを押したら、バックエンドから取得したデータを表示
- Flaskでバックエンドを作成
- リッスンポートは適当に8011とする
- フロントエンドへデータを引き渡すAPIエンドポイントを1つ用意
- pyweviewでWebブラウザ画面をデスクトップアプリウインドウ化し、reactで作成したフロントエンド部分を表示、実行
- PyInstallerで、フロントエンド機能、バックエンド機能、およびデスクトップアプリウインドウ機能を実行ファイル化
2. 必要なライブラリのインストール
2.1 Python
CORSを許可するため、flask-corsも入れる。
python -m venv .venv .venv/Scripts/activate pip install flask pip install flask-cors pip install pywebview pip install pyinstaller
2.2 node
axiosは、フロントエンドからバックエンドAPIを叩くために使用。
npx create-react-app frontend cd frontend npm install axios
3. Flask部
最低限の機能として、/helloというパスにGETを投げたら、"hello, world!"を返すようにしておく。
なお、flaskアプリケーションをスレッドで動作させるには、
use_reloader=False, threaded=Falseをつける
ということが、
https://blog.fantom.co.jp/2023/02/16/flask-signal-only-works-in-main-thread/
で記載されているので、その通り付加する。付加しないと、メインスレッドでないと動かない旨のエラーが出てしまう。 また、Reactアプリ側からアクセスするときにCORSが必要なので、CORSを有効化。
backend.py
import threading from flask import Flask, render_template from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/') def home(): return render_template('index.html') @app.route('/hello') def hello(): return {"message": "Hello, world!"} def main(): app.run(host='0.0.0.0', port=8011, debug=True, use_reloader=False, threaded=False) # スレッド server = threading.Thread(target=main, daemon=True)
4. React部
create-react-appのデフォルトのソースコードを少しだけ変更。バックエンドサーバの/helloにGETを投げるclickと表示されたボタンを1つ追加。データを受け取ったら、ボタンの名前の部分に受け取った文字列を表示する。
frontend/src/App.tsx
import logo from './logo.svg'; import React, { useState } from 'react'; import axios from 'axios'; import './App.css'; function App() { const [data, setData] = useState("Click"); const fetchData = () => { axios.get('http://127.0.0.1:8011/hello') .then(response => { setData(response.data.message); console.log(response.data); }) .catch(error => { console.error('Error fetching data:', error); }); }; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <button onClick={() => fetchData()}>{data}</button> </header> </div> ); } export default App;
ビルドしておく。frontend/buildディレクトリにファイル一式が作成される。
npm run build
5. pywebview部(メイン部)
pywebviewは、メインスレッドで動作させるため、webviewを実行するスクリプトをメインスクリプトとする。 webviewの初期表示画面として、reactで作成したフロントエンドアプリfrontend/build/index.htmlを指定。
import webview import backend # バックエンドサーバー機能を立ち上げる backend.server.start() if __name__ == '__main__': # WebViewを起動 window = webview.create_window('TestApp', 'frontend/build/index.html') webview.start()
6. 実行ファイル化
以下のコマンドで実行ファイルを作成する。--no-consoleは、実行ファイル起動時にコンソールウインドウを表示させないようにするため。また、--add-dataは、実行に必要な参照ファイルを一式含ませるために必要。今回は、frontend/buildフォルダに出力されているフロントエンドアプリファイル一式が必要なので、それを指定。reactを使わず、flaskでフロントエンドも作っている場合は、templatesフォルダなどが対象となる。
pyinstaller main.py --noconsole --add-data 'frontend/build:frontend/build'
実行後、dist/mainフォルダに実行ファイルが作成された。
7. 動作確認
dist/mainフォルダに作成された実行ファイルをダブルクリックして起動したところ、以下のように表示された。

ここで、clickボタンを押下したところ、無事バックエンドサーバーからデータを取得し、ボタンの文字列が変更された。

まとめと今後の課題
Flask+React+pywebview+PyInstallerでデスクトップアプリを作成することができた。早速、目的のアプリに適用していきたい。