LoginSignup
54
48

「Reflex(旧Pynecone)」があればPython 100%でWebアプリ作れるってマジ!!

Last updated at Posted at 2023-07-24

TL;NR;

Reflex(旧称はPynecone)っていうフレームワークを使うと、Pure PythonでかなりモダンなWebアプリが作れる。
2022年12月から開発が始まったばかりのようで日本語の記事が全然見つからないからサンプルコード組んでみた。

個人の雑感だけど、公式に"Build anything, faster."を謳ってる通り、コントロールのデザインが柔軟な上にレイアウトの種類がかなり豊富で、けっこう無理のきくページデザインができそう。
フルスタックなわりに初期化作業が簡単で取っ付きやすいし、データベースやコンポーネントも標準でしっかり用意されてて"Battery Included"な感じを受けた。

ただし新しすぎて検索しても情報は少ないから、公式ドキュメントがガチの生命線になる。
ともあれ新進気鋭でポテンシャルは高いから、ブレイクしたら類似フレームワークよりも人気が出るかも!?

インストール

Python 3.10.4 をインストール済みの Windows 11 とかで下記のコマンドを入力してインストールしたよ。

pip install reflex

コーディング

ReflexのトップページからChat GPTにアクセスしたり、DALL-Eで画像生成したりするサンプルコードにアクセスできるよ。
サンプルコードからして現代風ビュービュー吹かしてるよね!(ほめ言葉)

Hello world

私は技術力が低すぎてノーコードというかNo高度なので、そういうキラキラなコードについていけない。
なので古式ゆかしきHello worldを書くアプローチで攻めるとか言ってみるテスト(古語)。
今回はプロジェクト名をhello_reflexにしてみる。

  1. まず適当な場所にhello_reflexフォルダを作る
    ※フォルダ名は大体何でもよいが、reflexって名前のフォルダだと動かないので注意
  2. Powershellなどのターミナルを開いてhello_reflexフォルダにcdする
  3. reflex initを実行する
  4. フォルダの中に入れ子でhello_reflexフォルダが作られて、その中にhello_reflex.pyがあるのでテキストエディタで開く
  5. hello_reflex.pyの中身を全部消して、下記のように書き直して保存する
    import reflex as rx
    
    def index():
        return rx.heading("Hello Reflex!")
    
    app = rx.App(state=rx.State)
    app.add_page(index)
    app.compile()
    
  6. reflex runを実行する
  7. ブラウザで http://localhost:3000/ を開く

以降はreflex initは不要で、reflex runするだけでOK。
こんな感じの画面が出る。

Hello Reflex!

rx.Appでインスタンスを作って、そこにadd_pageでコンポーネントを返す関数を足すだけでWebページが出せる!
ちなみにreflex initの後の書き換え作業を飛ばしてreflex runすると、もう少し盛った下記の画面が出る。

Welcome to Reflex!

なんでわざわざソースコード書き換えたのかは疑問の余地が残るものの、そこそこシンプルなコードでWebページが作れることに納得してもらえただろうか。

簡単なInput

Reflexのデザインは、Stackなどのレイアウトコンポーネントの中に入力コンポーネントを詰め込んでいく方式。

PySimpleGUIとかでレイアウト組んでる人には親和性高めだと思う。
複雑な画面だと入れ子が増えるしプロパティの設定をしっかり定義すると記述量が増えるから、Exampleを見てると少し癖があると感じる人もいるかもしれない。

今回の簡単な画面でも、rx.vstack>rx.form>rx.vstack>rx.form_control>入力コンポーネントでそこそこ深い入れ子になってる。

import reflex as rx

class State(rx.State):
    number: float
    password: str
    number_text: str
    password_text: str

    def put_texts(self):
        self.number_text = f"1.2 + 0.5 = {self.number}" if self.number else ""
        self.password_text = f"パスワードは{self.password}" if self.password else ""

def index():
    return rx.vstack(
        rx.form(
            rx.vstack(
                rx.form_control(
                    rx.form_label("1.2 + 0.5 ="),
                    rx.input(on_change=State.set_number,placeholder="計算結果を入力",type_="float",is_required=True),
                ),
                rx.form_control(
                    rx.form_label("パスワード入れてね"),
                    rx.password(on_change=State.set_password),
                    rx.form_helper_text("助言は役に立たない"),
                ),
                rx.button("Submit", type_="submit"),
            ),
            on_submit=State.put_texts,
        ),
        rx.divider(),
        rx.text(State.number_text),
        rx.text(State.password_text),
    )

# Add state and page to the app.
app = rx.App(state=State)
app.add_page(index)
app.compile()

こんな感じに動く。

  • 初期表示
    初期表示
  • パスワード入力と必須エラー
    PasswordとError
  • 出力(Submitの下に表示)
    出力

入れ子が増える分、Streamlitよりは少しコード量が多くなるかな?

パラメータ保持用のクラス(今回はState)を作って、そこでデータを管理するのがReflexの特徴として目を引く。
このStateクラスは公式Examplesで必ず出てくるし、データを一か所に集めるのは良い設計だと思う。

それにしてもStateクラスにnumber変数作っただけで、set_number関数なんてないのにrx.number_input(on_change=State.set_number,が通るのはドキドキした。
(継承元のreflex.StateクラスにSettersが用意されてる)
最初は驚いたけど、コントロールと変数が簡単にデータバインディングできるのもいいよね。

複数コンポーネント

複数行のテーブルを表示する時もStateクラスを作って、rx.vstackの中に各種コンポーネント群を入れていく特徴は変わらない。

下記はちょっと長いコード例。
みどころはindex関数の中でrx.tableを定義(88行目)して、その中身はrx.foreach(State.rows, render_row)みたいに各行の処理を自作のrender_row関数に移譲してるところ。
それとrx.upload(92行目)以下で公式ドキュメントのcolorとかbgとかborderとかのデザイン関連のプロパティをふんだんに使ってるところ。

import reflex as rx
from typing import Any
from typing import List

class State(rx.State):
    war: str
    agree: str
    answer: str = "2"
    text: str
    rows:List[List[str]] = []
    img: list[str]

    # 公式ドキュメントを流用 https://reflex.dev/docs/library/forms/upload
    async def handle_upload(
        self, files: list[rx.UploadFile]
    ):
        """Handle the upload of file(s).

        Args:
            files: The uploaded files.
        """
        for file in files:
            upload_data = await file.read()
            outfile = rx.get_asset_path(file.filename)

            # Save the file.
            with open(outfile, "wb") as file_object:
                file_object.write(upload_data)

            # Update the img var.
            self.img.append(file.filename)

    def put_table(self):
        self.rows = [["好きなもの", self.war],
                     ["あなたは", "正直者" if self.agree else "嘘つき"],
                     ["1 + 1 =", self.answer],
                     ["Text Area", self.text],
                    ]

def render_row(row):
    return rx.tr(
        rx.td(row[0]), 
        rx.td(rx.markdown(row[1])),
    )
    
def index():
    color = "rgb(107,99,246)"
    return rx.vstack(
        rx.form(
            rx.vstack(
                rx.form_control(
                    rx.form_label("どっちが好き"),
                    rx.select(["きのこ","たけのこ","干し芋"], placeholder="究極vs至高",on_change=State.set_war),
                ),
                rx.form_control(
                    rx.form_label("チェックボックス"),
                    rx.checkbox("私は今Qiitaを読んでいます", on_change=State.set_agree),
                ),
                rx.form_control(
                    rx.form_label("1 + 1 ="),
                    rx.radio_group(["2","11",""], default_value="2", on_change=State.set_answer),
                ),
                rx.form_control(
                    rx.form_label("Markdown"),
                    rx.text_area(on_blur=State.set_text),
                ),
                rx.button("Submit", on_click=lambda: State.put_table()),
                #rx.button("Submit", type_="submit"),  # Submitにするならこちら
            ),
            #on_submit=lambda: State.put_table(rx.upload_files()),  # Submitにするならこちら
        ),
        rx.divider(),
        rx.table(
            rx.thead(
                rx.tr(
                    rx.th("項目"),
                    rx.th(""),
                )
            ),
            rx.tbody(
                rx.foreach(State.rows, render_row),
            ),
        ),
        # 公式ドキュメントを流用 https://reflex.dev/docs/library/forms/upload
        rx.upload(
            rx.vstack(
                rx.button(
                    "Select File",
                    color=color,
                    bg="white",
                    border=f"1px solid {color}",
                ),
                rx.text(
                    "Drag and drop files here or click to select files"
                ),
            ),
            border=f"1px dotted {color}",
            padding="5em",
        ),
        rx.button(
            "Upload",
            on_click=lambda: State.handle_upload(
                rx.upload_files()
            ),
        ),
        rx.foreach(
            State.img, lambda img: rx.image(src=img)
        ),
        padding="5em",
    )

# Add state and page to the app.
app = rx.App(state=State)
app.add_page(index)
app.compile()

こんな感じに動く。

  • ロード時
    ロード時
  • 入力後
    入力後

実はこのコードを書いてみたらかなり苦戦した。
Submit時に画像を表示しようとしたところ、画像がない時に表示が変わらなかったり、そもそもsubmitじゃ画像が表示できなくてon_clickに切り替えたりした。
さらにクリック時に呼び出す処理の呼び出しタイミングが独特らしく、単純なif文で処理を分岐させようとしてもうまくいかなかったり、Condを駆使してもやっぱりうまくいかなかったりもした。

前述のとおり「python reflex」で検索しても公式ドキュメント以外の情報がほとんどなかったり、フレームワークが新しすぎてかゆいところにプロパティが届かなかったりするので、まだそこは覚悟が必要だと思う。

画面遷移

画面遷移について、わりと簡単に書けた。
複数ページのプログラムなら、他の類似フレームワークより取り扱いやすいかもしれない。
ちなみに画面表示の関数名に_をつけるとURLは/に変換されてしまうので注意。

import reflex as rx

def index():
    return rx.vstack(
        rx.text("問おう、あなたが私のマスタード?"),
        rx.divider(),
        rx.link("はい(1へ行け)", href="/task/1"),
        rx.link("いいえ(14へ行け)", href="/task/14"),
    )

def task_1():
    return rx.vstack(
        rx.text("お腹がすきました!"),
        rx.divider(),
        rx.link("ごはんをつくる(Endingへ行け)", href="/ending"),
        rx.link("つくらない(14へ行け)", href="/task/14"),
    )

def task_14():
    return rx.vstack(
        rx.text("ざんねん!"),
        rx.text("あなたのぼうけんはここでおわってしまった!"),
        rx.divider(),
        rx.link(rx.button("コンティニュー"), href="/"),
    )

def ending():
    return rx.vstack(
        rx.text("おめでとうエプロンボーイ!"),
        rx.text("今日のごはんはごちそうだ!"),
        rx.divider(),
        rx.link(rx.button("戻る"), href="/"),
    )

# Add state and page to the app.
app = rx.App(state=rx.State)
app.add_page(index)
app.add_page(task_1, route="/task/1")
app.add_page(task_14, route="/task/14")
app.add_page(ending, route="/ending")
app.compile()

総括

python以外は1行も書かない縛りでかなり楽にリッチなWebアプリケーションを作ることができるフレームワークがまた一つ増えた。
開発中の機能も多いと思うけど、デザインの自由度が高くフルスタックなフレームワークに育ちそうだから、絶対にHTML書きたくないマンの救世主になるポテンシャルを秘めている可能性もそこはかとなくありそう。ってコンピューターおばあちゃんが言ってた気がする。
なんやかんやでRoRのアップデートに苦しんだり「React+TypeScriptって何だよ、pipすれば一発で動かせるくらい直感的になれよ!」って八つ当たりする我々のようなPython民は、戯れに試してどうぞ。

Reflexの公式サイトでホスティングサービスの応募をしてるから、興味のある方は申し込んでおくと吉。かもしれない。

54
48
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
54
48