フロントエンドプログラミング不要なハイレベルウェブフレームワーク、JustPyの紹介


# JustPyとは
[JustPy](https://justpy.io/)とは、フロントエンドプログラミングを必要としない、オブジェクト指向かつコンテンツベースなPythonのハイレベルウェブフレームワークです。
> An object-oriented, component based, high-level Python Web Framework that requires no front-end programming.

FlaskやDjangoなどでは、かなりの部分をPythonだけで完結できますが、それでもJinja2などを使用してHTMLを記載する必要があります。一方このJustPyは、本当にPythonだけで完結します。
※ただ結局のところHTMLやCSSの知識は必要ですが…

またハイレベルといっている通り、ベースはStarlleteというウェブフレームワークで、Asyncに対応しています。またフロントエンドはVue.jsをベースにしています。

以下は[公式のチュートリアル](https://justpy.io/tutorial/getting_started/)をthe model and data Attributesまでなぞった結果を自分なりにサマリしたものです。

# 簡単な例
HTMLの要素をクラスとして実装している。ウェブページ自体(表示するHTML全体)もWebPage()というクラスになっており、それをインスタンス化したものに他の子要素を追加していく形でコンテンツを追加していく。

```py3
import justpy as jp


def test():
    # p要素はP()というクラスで実装されている
    p = jp.P()
    p.text = 'This is test site.'

    # ウェブページ自体もWebPage()クラスのインスタンス。
    wp = jp.WebPage()
    # そこにp要素をaddする形でコンテンツを追加していく。
    wp.add(p)
    
    return wp


jp.justpy(test)
```

上記を実行すると、ブラウザでlocalhost:8000にアクセスすると、「This is test site.」と表示されます。

ちなみにまったく同じ内容をもっと短縮して記載することも可能。

```py3
def test_short():
    # ウェブページ自体もWebPage()クラスのインスタンスに。そこにp要素をaddする
    wp = jp.WebPage()
    p = jp.P(text='This is test site.', a=wp)
    return wp
```

# CSS
CSSについては[Tailwind](https://tailwindcss.com)といフレームワークを使うことを想定しているらしい。(フレームワークを使わない指定も可能)

```py3
def test_tailwind():
    wp = jp.WebPage()
    p_css = "w-64 bg-pink-500 m-2 hover:bg-pink-700 text-white font-bold py-2 px-4 rounded"
    jp.P(text='This is test site.', a=wp, classes=p_css)
    return wp
```

w-xxで幅、bg-xxxで背景色などを指定します。例えばwidthであれば、[公式ドキュメント](https://tailwindcss.com/docs/width/)を見ると定義がわかります。w-0が0.25 remに対応し、以降1毎に0.25 rem増えるようです。(64まで)
remはルート要素(通常HTML)のfont-sizeになります。また1/2など分数とすると、画面幅の1/2のような指定になります。
参考: https://qiita.com/butchi_y/items/453654828d9d6c9f94b0
このw-xxxという記載をutilityと呼んでいるようです。
m : margin
px,py: padding(pxは水平方向のマージンで、左右両端にとられる)
text, bg: 色。後ろの数字は100刻みで、大きいほど濃色になる。


# イベントハンドリング
イベントはPythonの関数にバインドされます。
マウスクリックなどのイベント発生 = 対応する関数の実行、です。

```py3
from pprint import pprint

def click(self, msg):
    self.text = 'Clicked.'
    # msgの内容確認のため
    pprint(msg)


def test_event():
    wp = jp.WebPage()
    d = jp.P(text='Not clicked.', a=wp, classes='w-64 m-2 p-1 bg-pink-500 text-white')
    d.on('click', click)
    return wp


jp.justpy(test_event)
```

イベントに対応する関数は2つ引数をとる必要があり、1つ目はイベントを生成するオブジェクト(selfとするのが推奨)、2つ目は発生したイベントの詳細(msgとするのが推奨)です。

```json
{'class_name': 'P',
 'event_current_target': '1',
 'event_target': '1',
 'event_type': 'click',
 'html_tag': 'p',
 'id': 1,
 'msg_type': 'event',
 'page': WebPage(page_id: 0, number of components: 1, reload interval: None),
 'page_id': 0,
 'session_id': '49bd7917e083441493a179bd85cda70d',
 'target': P(id: 1, html_tag: p, vue_type: html_component, name: No name, number of components: 0),
 'vue_type': 'html_component',
 'websocket': <starlette.websockets.WebSocket object at 0x0000024AE46CA400>,
 'websocket_id': 0}
```

# ルーティング
ルーティングも非常にシンプルで、Routeというクラスに、パスと呼び出す関数を指定するだけ。

```py3
def home():
    wp = jp.WebPage()
    wp.add(jp.P(text='This is Home.', classes='w-64 m-2 p-1 bg-pink-500 text-white'))
    return wp

jp.Route('/home', home)

jp.justpy()
```

これでlocalhost:8000/homeにアクセスすると表示されます。逆にhomeを指定しないとNot Foundになります。
同じ内容をデコレータを使って書くこともできます。Flask知っている場合はこちらの方がわかりやすいかも。

```
@jp.SetRoute('/home')
def home():
    wp = jp.WebPage()
    wp.add(jp.P(text='This is Home.', classes='w-64 m-2 p-1 bg-pink-500 text-white'))
    return wp
```

# インプット
インプットについても他のHTML要素と同様に用意されているInputクラスを使用します。

```py3
input_class = 'w-64 m-2 p-1 bg-pink-500 text-white'
output_class = 'w-64 m-2 p-1 bg-indigo-500 text-white'

async def input_test(self, msg):
    self.p.text = self.value

@jp.SetRoute('/input_test')
async def home(request):
    wp = jp.WebPage()
    input_box = jp.Input(a=wp, classes=input_class, placeholder='Inout')
    input_box.p = jp.P(text='Output', classes=output_class, a=wp)
    input_box.on('input', input_test)
    return wp

jp.justpy()
```

onに対してinputという固定のイベント名を指定してあげます。受け取ったイベントは対応する関数内で処理します。selfにイベントを発生させたオブジェクトが入ってくるので、そのテキスト内容を入力された内容に書き換えています。


# モデル
先の例では、入力されたテキストの情報はそのまま使われて保存されていませんでしたが、モデルの形で保存し、他のコンポーネントでも使用することができます。以下はInputに入力した内容を、別のコンポーネント(p要素)で表示しています。


```py3
@jp.SetRoute('/usecase2')
async def usecase2(request):
    wp = jp.WebPage(data={ 'text': 'Initial text'})
    jp.Input(a=wp, classes=input_class, placeholder='Please type here', model=[wp, 'text'])
    jp.P(model=[wp, 'text'], classes=output_class, a=wp)
    return wp
```

WebPageに対して、dataという引数で辞書形式のデータをモデルとして指定できます。Input要素、P要素の両方でmodelという引数でそのモデルを参照します。
参照の仕方が若干変わっていて、model=[wp, 'text']となります。「wpというインスタンスで指定されたモデルの中で、textというキーに対応するデータ」という意味らしい。
このモデルがどのように永続されるのか(できるのか)が不明なので調査中。

# 感想
モデルの永続化の部分を除けば、大体のことはFlask/Djangoより簡単にできそうです。また公式Tutorialを見る限り、かなり幅広い用途に使えそうです。次に何か自分で作る際には是非使用してみようと思います。