c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

Python で React Web front を書く

Web front は樂だ。UI が組み易い (だから皆 Electron を使ふのだ)。deploy & release し易い。そして今やどんな programming 言語ででも書ける。勿論 Python ででも書ける。

Transcrypt - Python in the browser - Lean, fast, open!

Transcrypt は Python から JavaScript への transpiler。Web server を Flask で書いて Web front をこれで書けば、Python しか讀み書き出來ない member が占めた project でも Web site を運用出來る。non 職業 programmer に餘り多くを義務附けるべきではない。

開發環境はWSL でも macOS でも、Docker で開發する - c4se 記:さっちゃんですよ ☆の手順で調へられてゐるとする。

Python 側で Transcrypt を入れる。私は Poetry を使ってゐる。node.js 側で React 等を入れる。build にはtranscrypt command を直截には使はずに Webpack を使ふ。

[tool.poetry.dev-dependencies]
transcrypt = "^3.7.16"
{
  "dependencies": {
    "create-react-class": "^15.6.3",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "transcrypt-loader": "^1.0.2",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-merge": "^4.2.2"
  }
}
"use static";

module.exports = {
  entry: {
    ui_main: "./ui_main.py",
  },
  module: {
    rules: [
      {
        test: /\.py$/,
        loader: "transcrypt-loader",
        options: {},
      },
    ],
  },
  target: "web",
};

build や配信は普通の Webpack の作法と成るので略する。

さて Python で Web front を書いてゆく。React.createElement()Python で書けばこれは JavaScriptReact.createElement()に成る。document.getElementById()fetch()等と書けばその通りに成る。よって JavaScript 側の API を知ってゐれば普通に書けると思ふ。Python の組み込み函數が使へるし、import すれば import した先も transpile されて Pythonic に呼べる。JavaScript の標準 API が加わった Python と考へれば好い。Python の async/await がそのまま JavaScript の async/await に成るのは嬉しい。Python 標準 library は一部しか使へないから、third party の library は transpile に失敗する事が多い。また JavaScriptAPI に直截 mapping されるから math 等 Python と Transcrypt とで結果が異なり得る。兩方で動く code は餘り夢想し過ぎない方が好いが、私は天文計算で實現したからそれも案外と出來る (# __pragma__("skip"), # __pragma__("noskip")を多用する事には成り、そこの計算は Web server へ任せなければならない)。最も注意すべき違ひは import である。Transcrypt は__init__.pyの中身を讀まず module 名だけから path を計算する。__init__.pyで何も再 export してゐない時と同じ樣に import すれば好い。それと format string f"{v}" が使へないから "{}".format(v) method を使ふ。

node_modules を呼ぶのに工夫が要る。Transcrypt 自體は node_modules の import を中途半端にしか行へない (少し行へてしまふのが罠だ)。そこで Transcrypt には「これは global に定義されてゐる」と誤認させ、Webpack に解決させる。例へば ReactDOM を mount する所では、

"""Main."""
from ui.components.App import App
import typing as t

__pragma__: t.Any = 0  # __:skip
document: t.Any = 0  # __:skip
React: t.Any = 0  # __:skip
ReactDOM: t.Any = 0  # __:skip
window: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const React = require("react");
    const ReactDOM = require("react-dom");
    """,
)

if __name__ == "__main__":
    window.addEventListener(
        "DOMContentLoaded",
        lambda event: ReactDOM.render(
            React.createElement(App, {}), document.getElementById("app")
        ),
    )
追記 2022-08-06 React 17 で JSX の transpile 方式が變更された。React 18 で React root の作り方が變更された。この二つを踏まへると、以下の記述になる。
"""Main."""
from ui.components.App import App
import typing as t

__pragma__: t.Any = 0  # __:skip
createRoot: t.Any = 0  # __:skip
document: t.Any = 0  # __:skip
jsx: t.Any = 0  # __:skip
jsxs: t.Any = 0  # __:skip
window: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const { jsx, jsxs } = require("react/jsx-runtime");
    const { createRoot } = require("react-dom/client");
    """,
)

if __name__ == "__main__":
    window.addEventListener(
        "DOMContentLoaded",
        lambda event: createRoot(document.getElementById("app")).render(jsxs(App, {})),
    )

skip pragma に依りそこは JavaScript へは吐かれない。js pragma は Transcrypt の解析を無視して JavaScript へ吐かれる。このrequire()が Webpack に依り處理される。

無理して JSX を使はずReact.createElementを使ってゐる。JSX を書かうとすればそこは Python としては文字列と成り、静的解析で間違ひを見附けられない。h()等の wrapper を書くのは好いと思ふ。

また JavaScript の class を継承する事は出來ない (extendsもであるし、JavaScript で「普通に」継承する - c4se 記:さっちゃんですよ ☆のやり方でも出來ない)。wrapper class を Python で書いてそれを継承する。React にはcreateReactClassが有るからこの方が樂だ。

"""ErrorBoundary component."""
import typing as t

__pragma__: t.Any = 0  # __:skip
console: t.Any = 0  # __:skip
createReactClass: t.Any = 0  # __:skip
React: t.Any = 0  # __:skip

__pragma__(  # noqa: F821
    "js",
    "{}",
    """
    const React = require("react");
    const createReactClass = require("create-react-class");
    """,
)


def renderErrorBoundary():
    """Render a component."""
    this: t.Any = 0  # __:skip
    if this.state.has_error:
        return React.createElement("a", {"href": "/"}, "Reload")
    return this.props.children


ErrorBoundary = createReactClass(
    {
        "componentDidCatch": lambda error, errorInfo: console.error(error, errorInfo),
        "displayName": "ErrorBoundary",
        "getInitialState": lambda: {"has_error": False},
        "render": renderErrorBoundary,
        "statics": {"getDerivedStateFromError": lambda error: {"has_error": True}},
    }
)


def supervise(*children):
    """Supervise children components & give a fallback UI."""
    return React.createElement(ErrorBoundary, {}, *children)

hooks も自然に使へる。[v, set_v] = React.useState(0)等とすれば好い。後は普通の React の世界と變はらない。lambda 構文で複數文を書けないから無名函數を使ふ機會は減るが、それは普通の Pythonic な世界だ。

帝國火星曆で實例が動いてゐる。SSR (server side rendering) は今後行ふ。