PythonでWebアプリが開発できるReflexを試してみた



本記事は 「 [TTDC Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ttdc) 」 19日目の記事です。

PythonでWebアプリケーションPythonだけで開発できる環境がないか探したところReflexという環境を見つけたので試してみました。

# トライ環境

[こちら](https://reflex.dev/docs/getting-started/installation/)のReflexのサイトを参考にトライ環境を構築しました。

使用したツールの詳細は以下の通りです。

- Reflex 0.6.2 post1
- Windows 11にWSL＋Ubuntu 20.04を用意
- Pythonの仮想環境
- Python 3.10.15(※1)
- SQLite3クライアント(※2)

※1)　*Reflex 0.6.0*からPython 3.8がdropされ Python 3.10以上になってます。（詳細は[こちら](https://github.com/reflex-dev/reflex/releases/tag/v0.6.0))<br>
※2)　SQLite3はReflexと一緒にインストールされますが、クライアントはインストールされないためデータベースの内容を確認するためには別途インストールが必要です

# 試作イメージ
ユーザーがブラウザーで登録したジョブをPCに配信、実行するWebアプリケーションを試しに作ってみました。

![app_struct.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959370/406a03d3-f4e4-5c16-2658-64b157f60be6.jpeg)


図中の「Job管理アプリ」は、ジョブ実行用に用意したPC上で実行され、ジョブ情報の取得、実行状態やPCの状態の通知等を行います。ジョブ実行用のPCの登録機能などは今回は対象外としました。
「Frontend server」には、実行したいタスクの登録や、配置したノードを確認するためのwebページでReflexのWebサイトに紹介されていたテンプレートをベースにしました。「backend server」は、「Frontend server」からのイベント処理に加え、「ジョブ管理アプリ」からのジョブ情報の参照やジョブやPC状態の更新を処理します。

なお今回はあくまでお試しなので、セキュリティの対策などは一切いれていません。

# プロジェクトの作成
最初にWebアプリケーション用のディレクトリを作成し、Reflexの初期設定を行います。このディレクトリ名がそのままメインのpythonファイル名になります。初期設定の際にテンプレートを選びますが、ReflexのWebサイトにあるtemplatesページに色々なデザインのWebページのサンプルがありますので、この中から使いたいテンプレートイメージを決めておきます。
```sh
mkdir project_dir
cd project_dir
reflex init --template template_name
```
なおtemplateオプションを指定しない場合、コマンド入力後に以下の様にテンプレートを選択する必要があります。
```
Get started with a template:
(0) blank (https://blank-template.reflex.run) - A minimal template
(1) dashboard (https://dashboard-new.reflex.run/) - A dashboard with tables and graphs
(2) sales (https://sales-new.reflex.run/) - An app to manage sales and customers
(3) ai_image_gen (https://ai-image-gen.reflex.run/) - An app to generate images using AI
(4) ci_template (https://cijob.reflex.run/) - A template for continuous integration
(5) api_admin_panel (https://api-admin-panel.reflex.run/) - An admin panel for an api.
(6) nba (https://nba-new.reflex.run/) - A data visualization app for NBA data.
(7) customer_data_app (https://customer-data-app.reflex.run/) - An app to manage customer data.
Which template would you like to use? (0): 
```

テンプレートによってREADME.mdが用意されており、テンプレートの中身の説明や実行方法などが記載されています。


# フロントエンドの実装
## Webページの実装
今回は下図のようなWebページを作成してみました。ページには登録されているPCのリスト、ジョブの登録用ボタン、登録済のジョブの状態を表示するリストを用意しました。

![webpage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959370/01237867-30da-7540-138f-df2e324155f6.jpeg)

Reflexで用意されているUI、レイアウト関連のコンポーネントの説明や使用方法は[Docsページ](https://reflex.dev/docs/getting-started/introduction/)の「Components」や「API References」、またそれらを組み合わせたFormのサンプル等は「Recipes」にあります。

ちなみにUI定義のコード内では、pythonの`if`文や`for`文表記は使用できませんので、専用の条件分岐やループ文を記載するためのコンポーネントを使用する必要があります。

デフォルトで用意されるコンポーネントを組み合わせて自分だけのコンポーネントを定義することも可能ですので、Reflex内のコンポーネントを組み合わせたコンポーネントを定義したり、[このページ](https://reflex.dev/docs/wrapping-react/overview/)を参考にして、まだReflexに含まれていないReactコンポーネントを使った独自のコンポーネントを定義することも可能です。

なお今回は使用しませんでしたが、複数ページのWebアプリを作成する場合は、 `reflex.app.App` クラスの `add_page(index)` を使用してWebページのルーティングを設定します。


# バックエンドサーバの実装

バックエンドサーバーに、Webページに配置したボタン等の各コントロールのイベント処理と、ジョブ管理アプリのためのAPI処理を実装します。


## イベント処理の実装
Webページで発生するボタンクリックやページの表示更新などのイベント処理のために`rx.State`クラスを継承したクラスを定義します。
```python
class WorkerState(rx.State):

    workers: list[Worker] = []

    def load_entries(self) -> list[Worker]:
        with rx.session() as session:
            query = select(Worker)
            self.workers = session.exec(query).all()
```

呼出し側となるWebページは、
```python
rx.table.root(
    rx.table.header(
        rx.table.row(
            _header_cell("Name", "snail"),
            _header_cell("Address", "monitor"),
            _header_cell("Status", "copy-check"),
        ),
    ),
    rx.table.body(rx.foreach(WorkerState.workers, show_workers)),
    on_mount=WorkerState.load_entries,
),
```
といった感じで参照します。


## データベースアクセス部分の実装
まず初めに使用するデータベースへの設定を`rxconfig.py`にて行います。
ReflexのデフォルトのデータベースであるSQLite3を使用する場合は、
```python
config = rx.Config(
    ...
    db_url='sqlite://<nohostname>/<path>'
    ...
)
```
を設定します。　もしも別のデータベースを使用する場合は、
```python
# PostgreSQLの場合：
db_url='postgresql://<user>:<password>@<host>/<path>'

# MySQLの場合：
db_url='mysql://<user>:<password>@<host>/<path>'
```
と使用するデータベースに合わせて設定します。

上記を設定後に、初期設定として以下のコマンドを実行します。
```sh
reflex db init
```
このコマンドによりAlembicの初期化とマイグレーション用スクリプトの生成等を行います。


次にデータベースに格納したいデータを、下記の例のように`rx.Model`のサブクラスとして定義します。
```python
class Worker(rx.Model, table=True):
    name: str
    address: str
    token: str
    status: str
    created_at: str
```
この例の場合、「name」、「address」、「token」、「status」、「created_at」の文字列型の各カラムを持つ`Worker`テーブルが生成されます。
ちなみに、`table`をFalseにするとデータベースにテーブルを生成しません。

なお、上記の`rx.Model`クラスを使ったORマッピング以外に、クエリを使用することも可能です。
クエリの使用方法の詳細については[こちら](https://reflex.dev/docs/database/queries/)を参照ください。

必要なデータクラスが定義できたら、以下のコマンドを実行しマイグレーションを実行します。
```sh
reflex db makemigrations --message 'something changed'
reflex db migrate
```
上記のコマンドを実行すると、クラスの定義内容に従ってデータベースにテーブルを生成、変更してくれます。

ちなみに、上記コマンドを忘れて`reflex run`コマンドを実行すると、「マイグレーション用のスクリプトを作成して」や「マイグレーションして」といった警告メッセージが表示されるので、マイグレーション忘れを防いでくれます。

更に複雑なテーブルを使用したい等の場合、OR Mapperに使用しているSQLAlchemy(詳しくはSQLAlchemyをラップしたSQLModelというライブラリ）や、マイグレーションのために使用しているAlembicのドキュメントを参照してください。


## APIサーバー部分の実装
Job管理アプリ用のAPIとして`/api/{worker_id}`というパスを用意し、ここにHTTP POSTメッセージ用のインタフェースを追加します。

インタフェースの追加は、`rx.App`を参照しているメイン部分に以下の処理を追加します。
```python
app = rx.App()
app.api.add_api_route(
    '/api/{worker_id}', report_worker_status, methods=['POST'])
```
第2引数にAPI呼出しがあった際に実行するコールバック関数を設定します。
(ここでは`report_worker_status`)

次にコールバック関数を定義します。
```python
from fastapi import Request

# POST:/api/{worker_id}
async def report_worker_status(worker_id: int, request: Request):
    ...
```

この定義に関してはReflexのドキュメントにもほとんど説明がなかったため、FastAPIのサンプルやドキュメントを参考にしましたが、
Web APIサーバー部分はFastAPIをベースにしているとのこともあり、FastAPIの知識が必要となりそうです。

その他実装上の注意事項として、この関数内からの`rx.State`サブクラスへのアクセスは行えない様です。
(あくまで今回試した結果の範囲になりますが、実際にアクセスしたところ`reflex.utils.exceptions.VarTypeError`というエラーが発生しました。このエラーについての説明はありませんでしたが、Reflexの構造やイベント処理の詳細については、[こちらのページ](https://reflex.dev/docs/advanced-onboarding/how-reflex-works/)に説明されています。)

`rx.Model`のサブクラスは問題なくアクセスでき、
```python
workers = Worker.select().where(Worker.id == worker_id).all()
```
といった感じで、データベースアクセスを行うことが可能です。


# プロジェクトの実行
作成したアプリケーションは、`reflex run`コマンドで実行することができます。
コマンドを実行すると、テスト環境としてローカルホスト上にフロントエンドとバックエンド用の各サーバーがそれぞれ起動されます。
```
App running at: http://localhost:3000
Backend running at: http://0.0.0.0:8000
```

ブラウザーなどから `http://localhost:3000` にアクセスすると作成したWebページを確認することができます。
また今回追加したWeb APIについては、下記の様に`curl`コマンドを使って動作を試すことができます。
```sh
 curl -X POST -v http://localhost:8000/api/1 -H 'Content-Type: application/json' -d ～
 ```

終了する場合は、実行中のコンソールで`CTRL^C`キーを入力します。


# 本番環境へのデプロイ
今回のトライでは行っていませんが、[このページ](https://reflex.dev/docs/hosting/self-hosting/)を参考に本番環境へのデプロイします。
ここでは、自分の用意したホスト(self-hosted)とDockerへのデプロイ方法について説明します。

- `rxconfig.py`の内容を本番環境への設定に変更します。
(本番環境のバックエンドサーバーURLや、データベースのアクセス先URL等の変更など。)　api_urlや、db_urlは環境変数(api_url→API_URL、db_url→DB_URL）として指定することも可能なのでファイルの編集は必須ではありません。
```python
config = rx.Config(
    ...
    api_url="http://app.example.com:8000",
    db_url='sqlite://<nohostname>/<path>',
    ...
)
```
- requirements.txtを作成します。(バックエンドのエクスポートファイルに含まれます)

※ 自身の用意したホストにデプロイする場合
- `reflex export`コマンドで作成したWebアプリをエクスポートします。既定動作ではzipファイルが生成されます。フロントエンド、バックエンドを別のzipファイルにエクスポートすることも可能です。
- 生成したzipファイルをapacheやNGINX等のWEBサーバーアプリもしくはuvicorn等のバックエンドサーバーアプリの所定のディレクトリに解凍します。
- 使用するサーバーアプリに従って設定を行います

※ Docker環境にデプロイする場合
- [こちら](https://github.com/reflex-dev/reflex/tree/main/docker-example)を参考に、用途に合ったDockerfileを使ってDocker imageを作成します。


# 最後に
今回Reflexを使用してみて気になった点などを最後にまとめておきます。

- 見た目がいい Webアプリケーションを素早く提供できる<br>
　→　pythonのみでReactベースのWebアプリを開発できるという点は魅力的
- 公式ドキュメント以外のドキュメントやサンプルがまだ少なめ<br>
　→　ベースに使用しているツールのドキュメントで補完できる場合もあります
- 事前に用意されているテンプレートに似た内容であれば非常に簡単に開発できる<br>
　→　少し複雑なことを行おうとするとベースに使用しているツールの知識が必要となることが多く、ハードルが一気に上がります
- 本番環境の構築～デプロイについては、やはり専門の知識が必要
　→　特にself-hosted環境へのデプロイの場合
- バージョンがまだ若い、という点で安定動作に少し不安が残る
- (フロントエンドとバックエンドのイベント処理部分)の負荷耐性がどの程度あるかは未知数
　→　Reflexの構造上ここがボトルネックになりそう
- 既存のマネージドサービスやWebAPI等の利用には工夫が必要そう
　→　基本はバックエンドから既存サービスにアクセスすることになりそう

マイナスイメージのことも挙げましたが、工夫次第で回避することも可能なこともあると思います。

とはいえ、基本的には小規模～中規模のWebアプリケーションをターゲットとした環境と考えた方がよいと思います。
今回試した内容は初歩的な部分のみですので実運用に用いるにはまだまだ不足する部分もあるかもしれませんが、様々なツールを利用してうまくラップ、パッケージ化することで、個々のツールを意識する必要なく単一言語(python)で開発を可能にするReflexのコンセプトは非常に面白く、魅力的なツールであると思いました。