今どきのウェブアプリ
Webアプリケーションで、画面が遷移しないで、刻々とグラフや数値が変わったり、地図が移動するものを見たことはありませんか?
そういうアプリはWebSocketを使い、サーバーと通信しながら画面の描画はJavascriptにやらせている可能性が高いです。
サーバーはHTMLの画面を送らずに、データだけを送受信するわけです。
アプリケーションによるけれども、だんだん、PHPでLaravelやCakeやCodeIgniterといった画面遷移することが前提のアプリケーションからWebSocketでのアプリケーションに多くが変わりつつあるように思えます。
ほら、だからPHPを覚えれば、Cakeを覚えれば、Larabelを覚えれば、Ruby On Railsを覚えれば、一生安泰なんてエンジニアの世界にはないのです。
今どきのフレームワーク
今更だけど、そういうアプリを作る必要が私にも出てきました。
サーバー側はあれこれやるので、Pythonが都合いい。
Pythonで「Webアプリ」「WebSocket」「非同期」とくると、Django, Flask, FastAPIなどが出てきます。
元祖はTornadoだと思います。
が、従来のフレームワークと完全に考え方が違うので、把握するのに苦労します。
逆にいえば、作った人はチョー賢い!
Tornadoはけっこう調べたのですが、難しい。アホでも書けるようにはなってないですね。
Djangoは昔、さわったことがあるのですが、WebサーバーにApacheがいります。
これは困ることが多いのです。
CGIのようにプログラムが呼び出されて、処理して、はい終わりだと、その情報は永続的なデータストア(要するにデータベースやcookie)に書き残さねばなりません。やり取りのたびに、データストアから情報を読んで思い出してアクションして、また書き残すってことを繰り返すって効率が悪くて仕方がありません。
データストアをし続けられないの?
だから今どきのフレームワークはプログラム中からウェブページを発行して、そのままレスポンスを待ってポートをリッスンするのです。
Webサーバーとしての機能がプアだとしても、今回はこれでOK.
WebSocketは見方を変えればずっとサーバー側と繋ぎっぱなしということです。
ブラウザの遷移状態を知っていなければなりません。例えばログインしてる、とか、地図見てる、とか。
FastAPI
とてもユニークな書き方ですが、わかりやすい。
これからのサーバーはデータの送受がメインになるはずです。
もちろんHTMLも送れます。
Python 仮想環境の起動
環境を作りたいフォルダーに移動して
python -m venv <env名>
<env名>のフォルダーができています。
起動の方法
source <env名>/bin/activate
インストール
FastAPIのパッケージとpythonでWebプログラムのテストをするuvicornをインストールします。
pip install fastapi uvicorn websocket
たとえばmain.pyというfastapiを前提としたプロうガム内にappというインスタンスがあるとします。
uvicorn main:app --reload
でサーバーが立ち上がるので、ブラウザーでアクセスします。
FastAPIを利用した最小限のwebsocketの使い方
サーバープログラム
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/")
async def get():
fh = open('test.html','r')
html = fh.read()
return HTMLResponse(content=html, status_code=200)
@app.websocket("/msg")
async def msg(websocket: WebSocket):
await websocket.accept()
while True:
request = await websocket.receive_json() #receive_text
msg = request["message"]
await websocket.send_json({"message": f"{request}"})
http://localhost:8000/ で、最初に目的のhtmlを送り出します。
htmlは以下のとおり、Javascriptが大事な部分です。
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<script>
var ws = new WebSocket("ws://localhost:8000/msg")
ws.onmessage = event => {
var msg = document.getElementById("msg")
data = JSON.parse(event.data)
msg.innerHTML = data.message
}
handleOnClick = () => {
ws.send(JSON.stringify({message: "OK"}))
}
</script>
</head>
<body>
<h1>WebSocket Test</h1>
<button onclick="handleOnClick()">Click</button>
<div id="msg">x</div>
</body>
</html>
ボタンを押すとJSONメッセージがid=”msg”のところに書き出されます。
とっても基本的ですが、ここまで絞らないとわかりにくいので絞り込みました。
さて、大事な追記があります。
FastAPIをいじってると、必ずstaticのマウントという意味がよくわからないものに出会うと思います。
これは最初に送ったhtmlと関係があります。
しばしばhtml内で「cssを別フォルダーにいれたい」「Javascriptを別のフォルダーで管理したい」ということで、html内に<link href=”css/style.css”>とか<script src=”js/sample.js”>とかしませんか?
FastAPIはこのようなHTML内から後ほどくるGETリクエストに限定的にしか動作しません。
(言い切るのは、これらすべてNot Found 404で返されるため)
そういう時、cssやjsをstaticフォルダーとして宣言してマウントしてください。
JSONとオブジェクト
なぜJSONを使うか?
それはデータとぞの属性を送受信することを考えると便利だからです。
具体的にはPythonもJavascriptも辞書型データにする機能をもっています。
Pythonの場合、
weather = {"sunny":"晴れ", "rain":"雨", "cloudy":"曇り", "snow":"雪"}
Javascriptの場合、
var wether = {sunny:"晴れ", rain:"雨", cloudy:"曇り", snow:"雪"};
ほとんど同じ表現です。
ですからサンプルのコードのように
Python側はreceive_jsonで受ければ、結果はオブジェクト(連想配列)に入ってくるし、送信はsend_json でオブジェクトを渡せばよい。
Javascript側はevent.dataをjson_parseでオブジェクト(連想配列)にし、送信はオブジェクトをjson_stringifyで渡せばよい。
これはとても便利で、なにかデータを渡したいならオブジェクトにどんどん追加すればいいということになります。(ただし、入れ子構造のJSONはオススメしません。処理が複雑になります。単純なキー:データがいいようです)
さて、最低限のところは理解できたので、エンジン開発をさらに進めます。