reactpy 試す
胡乱なプロダクトを見かけたので触ってみる。パッと見の疑問点としては
- Pythonでブラウザサイドコードが書けるわりにJSやWasmへの変換とかをやっている雰囲気を感じられない
kodaiさんの言うところによるとReactの再実装が行なわれているわけではなくreact, react-domに依存してはいるらしい。
ryeというやつを使ってみているがふつう (Rust基準) の体験が提供されている。
開発用組み込みStarletteサーバーがあるらしいが逆張り精神によってFastAPIを選択した。なんか起動するやつ (Uvicorn) も入れておく。
from fastapi import FastAPI
from reactpy import component, html, run
from reactpy.backend.fastapi import configure
@component
def App():
return html.h1("Hello, world!")
app = FastAPI()
configure(app, App)
uvicorn main:app
このへん謎といえば謎。Pythonはimportの順序だけ好き。
:8000で起動するのでアクセスするとh1にハロワが出る。
こういうやつを試すときにインタラクティブにしないのはエアプなのでカウンターを作るぞ。
from fastapi import FastAPI
from reactpy import component, html, run, hooks
from reactpy.backend.fastapi import configure
@component
def Counter():
count, set_count = hooks.use_state(0);
def handle_click(_):
set_count(lambda count: count + 1)
return html.div(
html.button({"on_click": handle_click}, "+1"),
html.span(f"count: {count}")
)
@component
def App():
return Counter()
app = FastAPI()
configure(app, App)
なんかさっきソースコード見てバックエンドフレームワークインテグレーションにWebSocketがある時点で嫌な予感はしていたのだが、アプリを起動すると ws://localhost:8000/_reactpy/stream
へのWebSocket接続が確立される。Hot module replacementだろうか?いや違う。WebSocketの通信が見える状態でボタンをポチポチすると、それに合わせて通信が行われているのが見える。
{"data":[{"altKey":false,"button":0,"buttons":0,"clientX":17,"clientY":21,"ctrlKey":false,"metaKey":false,"pageX":17,"pageY":21,"screenX":17,"screenY":140,"shiftKey":false,"target":{"boundingClientRect":{"x":8,"y":8,"width":27.26666259765625,"height":24.5,"top":8,"right":35.26666259765625,"bottom":32.5,"left":8},"value":"","name":""},"currentTarget":{"boundingClientRect":{"x":8,"y":8,"width":27.26666259765625,"height":24.5,"top":8,"right":35.26666259765625,"bottom":32.5,"left":8},"value":"","name":""},"relatedTarget":null}],"target":"1af2d818ff8e411c8f653869dc71f2a4"}
明らかに MouseEvent
である。
{"type": "layout-update", "path": "/children/0/children/0/children/0", "model": {"tagName": "", "children": [{"tagName": "div", "children": [{"tagName": "button", "eventHandlers": {"on_click": {"target": "1af2d818ff8e411c8f653869dc71f2a4", "preventDefault": false, "stopPropagation": false}}, "children": ["+1"]}, {"tagName": "span", "children": ["count: 10"]}]}]}}
明らかに仮想DOMの構造である。
要するに、
- インタラクションのたびにイベントデータがWebSocketでサーバに送信され
- それをサーバ上のPythonイベントハンドラが処理し
- その結果の仮想DOMがWebSocketで戻り
- クライアント側で実DOMに反映され表示される
ということらしい。もちろん開発サーバを止めればクライアントのインタラクションも死ぬ。
イベントハンドラはサーバサイドPythonであるため当然HTTP通信はサーバサイドで行われる。
import aiohttp
from fastapi import FastAPI
from reactpy import component, html, hooks
from reactpy.backend.fastapi import configure
@component
def Gallery():
image, set_image = hooks.use_state("")
loading, set_loading = hooks.use_state(False)
async def handle_click(_):
set_loading(True)
async with aiohttp.ClientSession() as session:
async with session.get("https://dog.ceo/api/breeds/image/random") as response:
set_image((await response.json())["message"])
set_loading(False)
if (loading):
return html.div("loading")
return html.div(
html.button({"on_click": handle_click}, "Roll"),
html.div(
html.img({"src": image, "width": 480})
)
)
@component
def App():
return Gallery()
app = FastAPI()
configure(app, App)
Loading表示を出すにも通信が必要なわけだが……
async with
(using
的なリソース管理のやつ) が無限にネスト段数増えていくのはなんとかならんものかね
WSで送られてくる仮想DOMをReactのアレに変換するのは単純にReactコンポーネントとして実装されている。まあ当然といえばそうかも。
そういえばhtml要素のlistを描画することはできるようなので、イテレータ操作を適当にやってリストに戻して描画とかはできそう。
Reactとの思想的距離が遠く、TypeScriptより静的型への気持ちが低い (今試したぐらいでもattributesの型がガバそう) とか無名関数に複文が書けないといった問題があるPythonを、ユーザーの一挙手一投足についてWebSocketとPythonコードの両面からリソースを消費してまでTypeScriptを差し置いて使うべきとは思わないが、面白い技術な気はした。変な技術を面白がっているだけだが。
一応ドキュメントにサーバサイドで動くということは書いてある のだが奥のほうに書いてあってintroにそのへんのアーキテクチャが書いてないのはどうなんだ。
という話をツイッターでしたら類似技術情報がいくつか流れてきた