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 で書けばこれは JavaScript のReact.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 に失敗する事が多い。また JavaScript の API に直截 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 な世界だ。