LoginSignup
1
1

Reflexの動作原理について

Last updated at Posted at 2024-05-31

お疲れ様です。

今日は「Reflexの動作原理」について部分いたします。

英語版は下記ページをご覧ください。
https://reflex.dev/docs/getting-started/how-reflex-works/

以下の基本的なアプリケーションを使用して、アーキテクチャの異なる部分を説明する例として、Githubプロフィール画像を表示するアプリケーションを使用します。

Screenshot 2024-05-31 at 11.36.24 PM.png

import requests
import reflex as rx


class GithubState(rx.State):
    url: str = "https://github.com/reflex-dev"
    profile_image: str = (
        "https://avatars.githubusercontent.com/u/104714959"
    )

    def set_profile(self, username: str):
        if username == "":
            return
        github_data = requests.get(
            f"https://api.github.com/users/{username}"
        ).json()
        self.url = github_data["url"]
        self.profile_image = github_data["avatar_url"]


def index():
    return rx.hstack(
        rx.link(
            rx.avatar(src=GithubState.profile_image),
            href=GithubState.url,
        ),
        rx.input(
            placeholder="Your Github username",
            on_blur=GithubState.set_profile,
        ),
    )

Reflexアーキテクチャ

フルスタックのウェブアプリケーションは、フロントエンドとバックエンドから構成されます。フロントエンドはユーザーインターフェースであり、ユーザーのブラウザで実行されるウェブページとして提供されます。バックエンドは、ロジックと状態管理(データベースやAPIなど)を処理し、サーバー上で実行されます。

従来のウェブ開発では、これらは通常2つの別々のアプリケーションであり、しばしば異なるフレームワークや言語で書かれています。例えば、FlaskのバックエンドとReactのフロントエンドを組み合わせることができます。このアプローチでは、フロントエンドとバックエンドを接続するために多くのボイラープレートコードを書く必要があり、2つの別々のアプリケーションをメンテナンスする必要があります。

Reflexでは、Pythonを使用して、フロントエンドとバックエンドを単一のコードベースで定義し、このプロセスを簡素化することを目指しました。開発者はアプリケーションのロジックについてのみ心配すればよく、低レベルの実装の詳細については気にする必要はありません。

TLDR

Reflexアプリは、ReactフロントエンドアプリとFastAPIバックエンドアプリにコンパイルされます。UIのみがJavaScriptにコンパイルされ、すべてのアプリのロジックと状態管理はPythonに残り、サーバーで実行されます。Reflexは、フロントエンドからバックエンドへのイベント送信と、バックエンドからフロントエンドへの状態更新を行うためにWebSocketsを使用します。

以下の図は、Reflexアプリの動作の詳細な概要を提供しています。後続のセクションで各部分について詳細に説明します。

https://reflex.dev/architecture.png

フロントエンド

Reflexアプリをエンドユーザーにとって従来のウェブアプリのように見えるようにし、同時に開発者にとっては簡単に構築および保守できるようにしたかった。これを実現するために、成熟した人気のあるウェブ技術をベースに構築しました。

Reflexでアプリを実行すると、Reflexはフロントエンドを単一ページのNext.jsアプリにコンパイルし、デフォルトではポート3000で提供します。これにより、ブラウザでアクセスできます。

フロントエンドの役割は、アプリの状態を反映し、ユーザーがUIとやり取りしたときにバックエンドにイベントを送信することです。実際のロジックはフロントエンドで実行されません。

コンポーネント

Reflexのフロントエンドは、複雑なUIを作成するために組み合わせることができるコンポーネントを使用して構築されています。HTMLとPythonを混在させるテンプレート言語を使用する代わりに、単純にPython関数を使用してUIを定義します。

def index():
    return rx.hstack(
        rx.link(
            rx.avatar(src=GithubState.profile_image),
            href=GithubState.url,
        ),
        rx.input(
            placeholder="Your Github username",
            on_blur=GithubState.set_profile,
        ),
    )

例えば、rx.hstackrx.avatarrx.inputなどのコンポーネントが含まれるサンプルアプリでは、これらのコンポーネントには外見や機能に影響を与える異なるプロパティがあります。たとえば、rx.inputコンポーネントにはデフォルトのテキストを表示するplaceholderプロパティがあります。

これらのコンポーネントは、ユーザーの相互作用に応じてon_blurなどのイベントに応答するようにすることができます。これについては以下で詳しく説明します。

内部では、これらのコンポーネントはReactコンポーネントにコンパイルされます。たとえば、上記のコードは次のReactコードにコンパイルされます

<HStack>
    <Link href={GithubState.url}>
        <Avatar src={GithubState.profile_image}/>
    </Link>
    <Input
        placeholder="Your Github username"
        // This would actually be a websocket call to the backend.
        onBlur={GithubState.set_profile}
    >
</HStack>

多くの基本的なコンポーネントは、人気のあるReactコンポーネントライブラリであるRadixに基づいています。また、グラフやデータテーブルなどの他の多くのコンポーネントもあります。

Reactを選んだのは、それが巨大なエコシステムを持つ人気のあるライブラリだからです。私たちの目標は、Webエコシステムを再作成することではなく、Python開発者にアクセス可能にすることです。

これにより、ユーザーが必要なコンポーネントを持っていない場合でも、独自のコンポーネントを持ち込むことができます。ユーザーは独自のReactコンポーネントをラップして、他の人が使用できるように公開することができます。時間の経過とともに、サードパーティのコンポーネントエコシステムを構築し、ユーザーが他の人が作成したコンポーネントを簡単に見つけて使用できるようにします。

スタイリング(Styling)

Reflexアプリがデフォルトで見栄えが良くなるようにしたいと思いましたが、開発者がアプリの外観を完全に制御できるようにしました。

私たちには、アプリ全体でダークモードやアクセントカラーなどの高レベルのスタイリングオプションを設定できるコアのテーマシステムがあり、統一された外観と感覚を与えます。

さらに、ReflexコンポーネントはCSSのフルパワーを使用してスタイリングできます。Emotionライブラリを活用して、「Python内のCSS」スタイリングを可能にし、任意のCSSプロパティをコンポーネントにキーワード引数として渡すことができます。これには、値のリストを渡すことでレスポンシブプロパティも含まれます。

バックエンド

次に、アプリに対してインタラクティブ性を追加する方法を見てみましょう。

Reflexでは、フロントエンドのみがJavaScriptにコンパイルされ、ユーザーのブラウザで実行されます。一方、すべての状態とロジックはPythonに留まり、サーバーで実行されます。reflex runを実行すると、デフォルトではポート8000でFastAPIサーバーが起動し、そのサーバーにフロントエンドがWebsocketを介して接続します。

すべての状態とロジックは、Stateクラス内で定義されています。


class GithubState(rx.State):
    url: str = "https://github.com/reflex-dev"
    profile_image: str = (
        "https://avatars.githubusercontent.com/u/104714959"
    )

    def set_profile(self, username: str):
        if username == "":
            return
        github_data = requests.get(
            f"https://api.github.com/users/{username}"
        ).json()
        self.url = github_data["url"]
        self.profile_image = github_data["avatar_url"]

状態は、変数とイベントハンドラーから構成されています。

変数は、アプリ内で時間とともに変化する可能性のある値です。これらは、Stateクラスのクラス属性として定義されます。任意のPythonのJSONにシリアライズできる型である可能性があります。私たちの例では、urlprofile_imageが変数です。

イベントハンドラーは、ユーザーがUIとやり取りしたときにStateクラス内のメソッドとして呼び出されます。これらは、Reflexで変数を変更する唯一の方法であり、ボタンをクリックしたりテキストボックスに入力したりするなどのユーザーのアクションに応じて呼び出されます。例えば、set_profileはurlとprofile_image変数を更新するイベントハンドラーです。

イベントハンドラーはバックエンドで実行されるため、これらの中で任意のPythonライブラリを使用できます。例えば、私たちの例では、requestsライブラリを使用してGithubにAPIコールを行い、ユーザーのプロフィール画像を取得しています。

イベント処理

ここで、イベントと状態の更新の扱い方について興味深い部分に入ります。

通常、ウェブアプリを書く際には、フロントエンドとバックエンドを接続するための多くのボイラープレートコードを書かなければなりません。Reflexを使用すると、その心配は必要ありません。フロントエンドとバックエンド間の通信を私たちが処理します。開発者は、イベントハンドラーのロジックを書くだけで済み、変数が更新されるとUIが自動的に更新されます。

上記の図を参照して、プロセスのビジュアル表現を見てみましょう。私たちのGithubプロフィール画像の例でこれを説明しましょう。

イベントトリガー

ユーザーは、ボタンをクリックしたり、テキストボックスに入力したり、要素の上にマウスを移動したりするなど、さまざまな方法でUIとやり取りすることができます。Reflexでは、これらをイベントトリガーと呼びます。

rx.input(
    placeholder="Your Github username",
    on_blur=GithubState.set_profile,
)

例では、on_blur イベントトリガーを set_profile イベントハンドラーにバインドしています。これは、ユーザーが入力フィールドに入力し、その後クリックしてフォーカスを外すと、set_profile イベントハンドラーが呼び出されることを意味します。

イベントキュー

フロントエンドでは、保留中のすべてのイベントのイベントキューを維持します。イベントは、次の3つの主要なデータで構成されます。

  • クライアントトークン:各クライアント(ブラウザタブ)には一意のトークンがあり、それによってバックエンドがどの状態を更新するかを知ることができます。
  • イベントハンドラー:状態で実行するイベントハンドラー。
  • 引数:イベントハンドラーに渡す引数。

たとえば、入力フィールドにユーザー名 "picklelo" を入力したとします。この例では、私たちのイベントは次のようなものになります:

{
    client_token: "abc123",
    event_handler: "GithubState.set_profile",
    arguments: ["picklelo"]
}

フロントエンドでは、すべての保留中のイベントのイベントキューを維持しています。

イベントがトリガーされると、キューに追加されます。処理フラグを持っており、一度に1つのイベントのみが処理されるようにします。これにより、状態が常に一貫しており、2つのイベントハンドラーが同時に状態を変更する競合状態が発生しないようにします。

イベントが処理の準備ができると、WebSocket接続を介してバックエンドに送信されます。

状態マネージャー

イベントが受信されると、バックエンドで処理されます。

Reflexでは、クライアントトークンとその状態とのマッピングを維持する状態マネージャーを使用しています。デフォルトでは、状態マネージャーは単なるメモリ内の辞書ですが、データベースやキャッシュを使用するように拡張することもできます。本番環境では、状態マネージャーとしてRedisを使用しています。

イベント処理

ユーザーの状態が得られたら、次のステップは引数を使ってイベントハンドラーを実行することです。

def set_profile(self, username: str):
    if username == "":
        return
    github_data = requests.get(
        f"https://api.github.com/users/{username}"
    ).json()
    self.url = github_data["url"]
    self.profile_image = github_data["avatar_url"]

例では、set_profile イベントハンドラーがユーザーの状態で実行されます。これにより、GithubにAPIコールが行われ、ユーザーのプロフィール画像が取得され、その後、状態の url と profile_image 変数が更新されます。

状態の更新

イベントハンドラーが返される(またはyieldされる)たびに、状態を状態マネージャーに保存し、UIを更新するためにフロントエンドに状態更新を送信します。

状態が成長するにつれてパフォーマンスを維持するために、内部的にReflexはイベントハンドラー中に更新された変数(dirty vars)を追跡します。イベントハンドラーの処理が完了すると、変更された変数を見つけて、フロントエンドに送信するための状態更新を作成します。

私たちの場合、状態更新は次のようなものになるかもしれません:

{
    "url": "https://github.com/picklelo",
    "profile_image": "https://avatars.githubusercontent.com/u/104714959" 
}

新しい状態を状態マネージャーに保存し、次に状態更新をフロントエンドに送信します。その後、フロントエンドは新しい状態を反映するためにUIを更新します。例えば、私たちの場合では、新しいGithubプロフィール画像が表示されます。

今日は以上です。

ありがとうございました。
よろしくお願いいたします。

1
1
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
1
1