Flask+JSでPOSTする方法(form,jquery,XMLHttpRequest)  （Flask(python)→JSの変数渡しについても少々）

Flaskでアプリを作っていると、だんだん複雑な処理がしたくなってきます。
Flaskでのjsの使用は、以下の問題と隣り合わせです。

・Flaskやjinjaの機能でどこまで実現できるのか分かりずらい
・PythonでJSON使うと文字化けする
・pythonからjsへ変数を渡すのが困難

こうした状況なので、試行錯誤した中で見えてきたやり方をまとめてみようと思ったものです。

まず最初の「Flaskやjinjaの機能でどこまで実現できるのか分かりずらい」という話。FlaskではGETメソッドの機能は充実しています。それが`{{ url_for }}`です。
例えば、HTMLの中で
```sample.html
<a href='{{ url_for('/top') }}>リンク</a>'
```
で/topへ遷移するリンクとなります。また、引数も与えることができ、
```sample.html
<a href='{{ url_for('/top', namae=taro,id=001) }}>リンク</a>'
```
でクエリパラメータを組めます。
このjinjaの便利な機能はGETメソッドでの送信です。可変長引数にすると受信側でややこしい処理が必要になりますし、データの中身によってはPOSTで送りたいということが出てきます。
しかしPOSTをするための標準機能は準備されていません。そこで、javascriptを使おうという話になります。
すると今度は、Pythoからjsへの変数渡しの制約が出てきます。先に言ってしまうと、pythonからjsへの変数渡しのためには、HTMLにscriptタグでベタ書きする必要があります。また、渡せるのはJSON形式へ変換したデータになります。ですので、ここで解説するコードではstaticフォルダは出てきません。
なお、今回扱うのはキー&バリュー型のテキストデータです。画像などその他のファイルの送信についても参考になる点もあるかもしれませんが、この記事では直接は触れません。

### 解説するアプリ
```
.
├── app.py
└── templates
    ├── index.html
    └── sub.html
```

```app.py
from flask import Flask, render_template, request
import json

app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False #文字化けを防ぐ方法その1

@app.route("/")
def index():
    print('/indexです')
    print('')

    book_list = [
        {'title':'Learning Node, 2nd Edition','page':'288'},
        {'title':'カオスエンジニアリング','Page':'316'}
        ]
    book_json = json.dumps(book_list,ensure_ascii=False) #文字化けを防ぐ方法その2 ensure_ascii=False をつけるとutf-8でエンコードされる

    birds_list =  ['シマエナガ','メジロ','カワセミ']
    return render_template('index.html', books=book_json, birds=birds_list)


@app.route("/sub",methods=['GET','POST'])
def sub():
    print('/sub です')
    print('')

    # get_dataは中身をそのまま取り出す。
    s_get_data = request.get_data()
    print("get_dataの中身")
    print(type(s_get_data)) # → <class 'bytes'>
    print(s_get_data) #テキストの扱いには不向きかも😢
    print('')

    # .formメソッドは"使える"。
    s_form = request.form
    print("request.formの中身")
    print(type(s_form)) # → <class 'werkzeug.datastructures.ImmutableMultiDict'>
    print(s_form) # 正解👍
    print('')
    if s_form:
        print(s_form.get("ポケモン1")) #これで取り出し可能
        print(s_form.to_dict(flat=True)) #dictへ変換
        print(s_form.to_dict(flat=False)) #dictへ変換
    return render_template('sub.html', s_form=s_form)


if __name__ == '__main__':
    app.run(debug=True)
```

```index.html
<htmlall> <p>
  <!DOCTYPE html>

  <head lang="ja">
    <meta charset="utf-8" />

    <!-- ボタン1 -->
    <script>
    function LinkClick1() {
        var f = document.forms["form1"];
        f.method = "POST"; 
        f.submit(); 
        return true;
      }
    </script>

    <!-- ボタン2 -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script>
    function LinkClick2(){
      $.post( '/sub', 'ポケモン1=ラティアス&ポケモン2=ラティオス' )
      .done(function( data ) {
        $("htmlall").html(data);
        })
      };
    </script>
    <!-- ボタン3 -->
    <script>
      var myBooks = {{ books|tojson }};
      function LinkClick3(){
        let testRequest = new XMLHttpRequest();
        testRequest.open('POST', 'sub');
        testRequest.setRequestHeader('content-type', 'application/json');
        testRequest.send(myBooks);
      }
    </script>

    <!-- ボタン4 -->
    <script>
      var myBirds = {{ birds|tojson }};
    </script>

    <!-- ボタン5 -->
    <script>
    function LinkClick5() {
      location.href = '/sub';
    }
    </script>

    <title>ここはトップページ</title>
  </head>


  <h1>トップ</h1>
  <!-- ボタン1 -->
  <button onclick="LinkClick1();">ボタン1(formの値をPOST)</button>

  <form name="form1" action="sub">
    <input type="hidden" name="ポケモン1" value="ソーナノ">
    <input type="hidden" name="ポケモン2" value="チルット">
  </form>

  <!-- ボタン2 -->
  <button onclick="return LinkClick2();">ボタン2(jqueryでPOSTしてページ遷移、返り値のHTMLでHTML全体を書き換え)</button> 

  <!-- ボタン3 -->
  <br>
  <button onclick="LinkClick3();">ボタン3(XMLHttpRequestでPOST)</button>

  <!-- ボタン4 -->
  <br>
  <button onclick="alert('birds: ' + myBirds.join(', '))" >ボタン4(渡されたbirdsオブジェクトをjsonにしてアラート)</button>

  <!-- ボタン5 -->
  <br>
  <button onclick="LinkClick5();">ボタン5(js側でページ遷移)</button> 

</p></htmlall>
```

```sub.html
<!DOCTYPE html>

<head lang="ja">
  <meta charset="utf-8" />
  <script type="text/javascript" src="{{ url_for('static', filename='sample.js') }}"></script>
  <title>ここはサブページ</title>
</head>

<h1>サブページ</h1>

<br>
<span>s_formの中身:</span><br>
{{ s_form }}

<br>
<a href="{{ url_for("index") }}">Topページへ</a>
```


#### 確認する方法3つ（+おまけ2つ）
ボタン1 : form形式でPOST。ページ遷移あり。
ボタン2 : jqueryでPOST。ページ遷移あり（ちょっとトリッキー）
ボタン3 : XMLHttpRequestでPOST。ページ遷移なし。

以下おまけ
ボタン4 : pythonから渡されたList型データをjsでJsonに変換してアラート表示
ボタン5 : js側でページ遷移。これはGETメソッド。

#### 解説
このアプリでは何をしているかというと、二つのHTMLおよびpythonとjs間で、主にPOSTを用いてデータをやり取りする方法をいくつか試しています。動かしながら順に解説します。
トップページ：
![スクリーンショット 2022-08-26 3.35.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/f3947027-72ea-fb26-cb76-cd24fd2944b3.png)

##### ボタン1

![スクリーンショット 2022-08-26 3.37.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/79ed17d2-9bb8-ddec-5843-2aa90152bc50.png)

![スクリーンショット 2022-08-26 3.37.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/c4103dd0-f315-e1b8-4443-5af2ee0cdbc9.png)

ボタン1では、HTML・jsのformを使った送信を行なっています。まずHTMLですが、
```
<form name="form1" action="sub">
  <input type="hidden" name="ポケモン1" value="ソーナノ">
  <input type="hidden" name="ポケモン2" value="チルット">
</form>
```
でformを作っています。type="hidden"を指定することで表示されないことを利用し、id等の見せる必要のないデータの送信を行うのに応用が効きます。
formを使った方法にはメリットがあります。受信側で便利なメソッドが用意されていることです。それが`request.form`です。
app.pyの`def sub()`関数では、まず`request.get_data()`でbodyの取り出しを試しています。その結果、bytes型の扱いずらそうな形のデータが取り出されていることが分かります。このように、get_data()メソッドはbodyの中身をそのままbytesで取り出します。一方、その後に試している`request.form`では、MultiDictという形式で綺麗に取り出せていることがわかります。
formでPOSTされたデータは、このように、Flask側で簡単に取り出せます。また、その後のdictへの変換や、キーでの取り出しもサポートされていることがわかります。
formを使ったこのPOSTのやり方は最初見た時は意味がわからなかったのですが、今はこのやり方が最もスマートなのかなと思っています。


##### ボタン2
![スクリーンショット 2022-08-26 3.51.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/2279ccc6-f4d2-825a-f482-2c02b52bf91f.png)
![スクリーンショット 2022-08-26 3.52.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/897510cb-25ca-08a4-19db-c005be91b86d.png)

このやり方は、formを使わずjqueryのメソッドを用いてPOSTするものです。
送信するデータはjsの関数内で作っています。
POST後、FlaskからHTMLが返ってきますが、それをHTML全体に挿入することでページを書き換えています(このやり方は教えてもらうまでかなり悩んだ)。そのためにHTMLファイル全体がタグで囲まれています。ページ遷移のやり方はもっとスマートな方法がある気がしますが、ひとまずこれで動くには動きます。
この方法の利点としては添付データを作りやすいことでしょうか。ここでは`'ポケモン1=ラティアス&ポケモン2=ラティオス' `というデータを送信していますが、辞書ぽい書き方もできるはずです。いずれにせよFlask側のrequest.formでいい感じに解釈してくれます。

##### ボタン3
このボタンは、押しても画面遷移しません。でもPOSTでデータを確かに送信しています。
![スクリーンショット 2022-08-26 4.04.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/aa42d2be-3997-f7c5-6d32-91236205a793.png)
![スクリーンショット 2022-08-26 4.05.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/c6c27f08-4360-e57a-6f1a-4179161a46a4.png)

AjaxのXMLHttpRequestを使ってますが、やっていることはボタン2とほぼ同じです。ボタン2の方ではHTMLを書き換えましたが、それをしていません。
受信側のrequest.form()が空オブジェクトを返していますね。これは`Content-Type`ヘッダーが `application/x-www-form-urlencoded
`ではなかった時の挙動です。jqueryのPOSTはデフォルトで`application/x-www-form-urlencoded`になっているそうです。`XMLHttpRequest`を使うメリットは、、、IE対応？でしょうか。わざわざ採用することはなさそうです。
ここで渡しているJSONですが、topページからの遷移時に渡されたJSONをもとに、`var myBooks = {{ books|tojson }};`でjs側で扱えるようにしたものです。pythonからjsへデータを渡す方法をいろいろ探してみたのですが、このやり方しか見つかりませんでした。元がJSONでも、このように、再度tojsonを使って渡すことが可能です。しかしtojsonで変換する際は、元がリスト型の方が綺麗に受け渡せるかもしれません。それが次のボタン4です。


##### ボタン4
ここからはPOSTとは関係のない話です。
![スクリーンショット 2022-08-26 4.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/7eda8300-c1c5-5188-b0aa-1f9ea222856b.png)
ここではPythonからjsへの変数渡しを試しています。
jsスクリプトを見ていただくと分かるとおり、受け取ったリスト型を`{{ |tojson }}`でJSONへ変換しています。そして綺麗に扱えています。jsへ文字列や配列を渡すのであれば、リスト→jsonが最も良いのかもしれません。
ボタン3とボタン4で紹介したような、python→jsのデータ受け渡しには、HTML内にscriptタグでjsを記述する必要があります。それをしないのであれば、staticフォルダ下に作った方が整理に良いと思います。

##### ボタン5
これは、{{ url_for }}を用いずに、jsでページ遷移を行う方法です。
![スクリーンショット 2022-08-26 4.27.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158553/80688f8a-2e4e-5cee-0c9d-47554982b4e4.png)

`location.href = '/sub';`これだけで遷移ができるので、簡単ですね。これはGETメソッドになります。
もちろんクエリパラメータの操作もいろいろメソッドがあります。GETメソッドでよければこの他にもaタグでurlを直書きするといった方法ももちろんあります。


#### 文字コードについて
実はPOSTのやり方を探る上で一番厄介だったのが、冒頭で書いた、文字コードの問題でした。
json.dumps()などのメソッドを使いpythonでjsonをいじると、だいたい、文字化けしますよね。
json.dumps()で文字化けさせない方法として、ソースコードに書いた二つの方法があります。一つは `app.config["JSON_AS_ASCII"] = False` で設定するやり方。もう一つはjson.dumps()時に`ensure_ascii=False`を渡してやるやり方です。それでも上で見たように完璧ではありません。マルチバイト文字を使わない設計であれば、悩むことはだいぶ減ると思います。

#### 感想
POSTを使ったデータ送信でrequest.formを使った方法がやたらとヒットするので悩んでいたのですが、だいぶ腑に落ちました。基本的にはpythonの受け手側のrequest.formに気を配った形で設計していけば間違いないだろうと思います。


#### 参考
https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/
https://qiita.com/nagataaaas/items/24e68a9c736aec31948e
https://qiita.com/sai-san/items/1d1f02dc4dcaa7902392
https://zenn.dev/ikaro1192/books/999b71a570cb89024716/viewer/beb222e301d2119669cc
https://qiita.com/bow_arrow/items/194c1f3b1211d7892ab8
https://qiita.com/aKuad/items/400550b76d79c0d2cd3c
https://api.jquery.com/jQuery.ajax/
https://qiita.com/EZ_Denta/items/e485207daaba68c52550
https://stackoverflow.com/questions/21039680/return-json-will-redirects-to-another-view-when-url-specified
https://atmarkit.itmedia.co.jp/ait/articles/1604/20/news030.html
https://segakuin.com/javascript/jquery/global/post.html