FastAPIでの静的ファイル, index.htmlの設定(WebSocket Chatの例を直す)

アドベントカレンダー8日目です。カレンダー空いてました！やばい！
昨日は[@sh05_sh05](https://qiita.com/sh05_sh05)の[vimでPDCAを回す](https://qiita.com/sh05_sh05/items/2305b5c038d51c249f4f)でした！

FastAPIの静的ファイルの置き方法についてです。
扱うコードは公式ドキュメントのWebSocketの例を利用します。

アドベントカレンダーにしては狭いトピックですがFastAPIの紹介とWebSocketによるchatとドキュメントが充実しているよという意味合いに取ってください。

***記事として冗長なので解決したくてアクセスされた場合は本編に飛んでください***

## FastAPIとは

Pythonの軽量ウェブフレームワークです。素晴らしさなどは他のページ、サイトで十分説かれているのでこの記事では公式より

>
- Fast: Very high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic). One of the fastest Python frameworks available.
- Fast to code: Increase the speed to develop features by about 200% to 300%. *
- Fewer bugs: Reduce about 40% of human (developer) induced errors. *
- Intuitive: Great editor support. Completion everywhere. Less time debugging.
- Easy: Designed to be easy to use and learn. Less time reading docs.
- Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
- Robust: Get production-ready code. With automatic interactive documentation.
- Standards-based: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.

>[FastAPI](https://fastapi.tiangolo.com/)

と引用しておきます。
ドキュメントが自動で生成されたり、公式ドキュメントが充実していてとても好きです。
## 扱うコード

このページの最下部からです。
[WebSockets - FastAPI](https://fastapi.tiangolo.com/advanced/websockets/#handling-disconnections-and-multiple-clients)

```python3:websocket_chat.py
from typing import List

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse

app = FastAPI()

### 割愛 ###

@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.send_personal_message(f"You wrote: {data}", websocket)
            await manager.broadcast(f"Client #{client_id} says: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"Client #{client_id} left the chat")
```

そのまま動かせばWebSocketを利用したChatアプリができます。すごいですね。

その中にこんな2箇所があります。

```python3:websocket_chat.py
html = """
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <h2>Your ID: <span id="ws-id"></span></h2>
        <form action="" onsubmit="sendMessage(event)">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var client_id = Date.now()
            document.querySelector("#ws-id").textContent = client_id;
            var ws = new WebSocket(`ws://localhost:8000/ws/${client_id}`);
            ws.onmessage = function(event) {
                var messages = document.getElementById('messages')
                var message = document.createElement('li')
                var content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                var input = document.getElementById("messageText")
                ws.send(input.value)
                input.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
"""


@app.get("/")
async def get():
    return HTMLResponse(html)
```
明示的にHTMLと宣言して文字列を返しているようです。
もちろん注意書きで本番環境には適さない、フロントとちゃんと連携しろなどと書いてあります。

少しいじって試そうと思ったときに、（設定してなければ）シンタックスハイライトが効きづらいし、（折り畳みが無効であれば）ファイルが長くなります。


なので今回はFastAPIの静的ファイル、特にHTMLの配置をやります。（特に強い理由はないですが練習としてやっていきます）

## 読むページ
[Static Files - FastAPI](https://fastapi.tiangolo.com/tutorial/static-files/)
このページを見てできそうですが、ベースになっているStarLetteのドキュメントを読むとHTML用のオプションもあったのでそちら（下記）を読みます。
余談ですがresponderもStarletteがベースになっているそうです。

[Static Files - Starlette](https://www.starlette.io/staticfiles/)
こちらを読んでみると

>html - Run in HTML mode. Automatically loads index.html for directories if such file exist.

とあります。単なる静的ファイルとしてアクセスだけでなくindex.htmlは自動で読み込んでくれるそうです。

## 本編

### 構成

```
app
├── html
│   └── index.html
└── websocket_chat.py
```

### aiofilesのインストール

```
pip install aiofiles
```

### コード
```python3:websocket_chat.py

from typing import List

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles

app = FastAPI()


app.mount("/chat", StaticFiles(directory="/app/html", html=True), name="html")

# 以降は変更なし
```
`app.mount`がルーティングとなっています。
そして先述の通りindex.htmlを読み込むように指定したので`@app.get`の追加などをすることなく`/chat`にアクセスするとできます。
![スクリーンショット 2020-12-08 17.17.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176375/4401f0c1-4d74-5665-fcf1-238095013f46.png)

おわり