Help us understand the problem. What is going on with this article?

JavaScriptでfileをfetchしてFlaskで処理させる

More than 1 year has passed since last update.

はじめに

これは,SLP KBIT Advent Calendar 2018 3日目の記事です.
前日の記事は こちら

今回やること

JavaScriptで画像ファイルを/api/picsにfetch(POST)して,
(Flaskで処理した後に)画像を/picsで表示させること

環境

Windows10, 1803
WSL, Ubuntu, 16.04.4 LTS
Python, 3.5.2
Firefox, 63.0.3

ディレクトリ構造

app.py
templates/
 ┣ layout.html #ベースとなるHTML
 ┗ pic.html #layout.htmlを継承させている
static/
 ┣ js/
 ┃ ┗ main.js
 ┣ css/
 ┃ ┗ styles.css
 ┗ pic/
   ┗ アップロードされた画像ファイル

送信ボタンを押してファイルを送信する

image.png
ここで,ファイルを選択して送信すると,/api/picsにファイルをPOSTして,
/picsに画像が表示されるようにします.

templates/pic.html
<!--画像投稿フォーム-->
<input type="file" name="img_file" id="img_file" class="col-sm-5">
<input type="submit" id="submit_btn" value="送信">

<div id="alter-album"></div>
<script src="/static/js/main.js"></script>

formからPOSTしてみる

templates/pic.html
<form id="file_post" method="post" action="/api/pics" enctype="multipart/form-data">
  <input type="file" name="img_file" id="img_file" class="col-sm-4">
  <input type="submit" id="submit_btn" value="送信">
</form>

とすると,POSTはできますが,/api/picsに移動します.
actionを使っているので当然ですね.
image.png

fetchで投げてみる

static/js/main.js
let formData = new FormData();

const upload = () => {
  const btn = document.getElementById('submit_btn'); //送信ボタン
  file = document.getElementById('img_file'); //file
  btn.disabled = true; //連打防止
  btn.value="送信中";
  fetch('/api/pics', {
    method: 'POST',
    body: formData ,
  }).then(res => res.json()
  ).then(json => {
    if(json["status"] == "false"){ //Flask側で"false"と判断されたらアラートする
      alert(json["message"])
    }
    afterPost(); //POST後の処理
  })
};

const onSelectFile = () => upload();
document.getElementById('submit_btn').addEventListener('click', onSelectFile, false);

/*--------------------------------------------*/

window.addEventListener("load", () => {
  const input = document.getElementById('img_file');
  input.addEventListener("change", () => {
    formData.append('img_file', input.files[0]);
  });
})

投げられたので,再読み込みします.

static/js/main.js
function afterPost() {
  const btn = document.getElementById('submit_btn');
  btn.disabled = false;
  btn.value="送信";
  renderPictures();
}

/*------画像を読み込む------*/
function renderPictures() {
  const album = document.getElementById('alter-album');
  while (album.firstChild) album.removeChild(album.firstChild);
  getPictures().then(pictures => {
    pictures.forEach(picture => {
      let img = document.createElement('img')
      img.src = picture
      img.addEventListener("click", () => deletePicture(picture))
      album.appendChild(img)
    })
  })
}

function getPictures() {
  return fetch("/api/pics")
    .then(res => res.json())
}

送信後フォームがリセットされるようにする

このままだと,送信後も下のように送信したファイルが残ってしまいます.
image.png

送信後,ファイル名を消しましょう.

input.value = "";

ファイル名はリセットされますが,送信ボタンを押すと,以前送信したファイルが送信されてしまいました.
image.png

FormDataをnewしよう

ファイルを送信したら,FormDataをnewするようにします.

static/js/main.js
function get_func(url) {
  const input = document.getElementById('img_file');
  const btn = document.getElementById('submit_btn');
  btn.value="送信";
  input.value = "";
  formData = new FormData(); //ここでnewさせる
  fetch(url)
    .then(() => renderPictures())
}

これで,ファイルを送信した後にフォームがリセットされました.

ボタンを押せないようにする

エラーを防ぐためにも,そもそもファイル名が無い場合,ボタンを押せないようにすれば良いということに気づきました.const upload内にvalue = "";のときは送信されないように記述します.

static/js/main.js
  if (!file.value){
    return false;
  }

ただ,押せなくなったので,formDataをnewしなくても問題ないというわけではなく,次に述べる問題が発生した際,newしないと正常なファイルが送信できなくなってしまいます.

問題点

ファイルサイズの上限

app.py
#アップロードの上限サイズを1MBにする
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024

このように,上限を1MBに設定しました.

app.py
try:
    img_file = request.files['img_file']
except RequestEntityTooLarge as err:
    return jsonify({'status': "false",
                    'message': "アップロード可能なファイルサイズは1MBまでです"})

fileサイズが1.3MB程度の時は,app.pyでエラー(413)と判断してjsonをresponseしていたのですが,3MBなど超過が大きくなると,下のようにFlaskからresponseが返ってこなくなりました.
TypeError: "NetworkError when attempting to fetch resource."
試しに他のブラウザでも検証.

ブラウザ 結果
Firefox, 63.* 上限を越えていても,サイズが1MB程度の時はres有り(413)
Edge, 42.* responseが返ってくる(200)
Chrome, 70.* responseが返ってこない

検証してみた結果として,ファイルサイズの上限を500KBにして,投稿するファイルのサイズが1MB程度の時は,413のresponseが返ってきました.上限が1MBのときと同じく,ファイルサイズが3MB程度になると,responseが返ってこなくなりました.このことから,ブラウザ側のセキュリティ関連の問題かと疑ったのですが解決には至りませんでした.原因がわかり次第追記します.

responseがとれないのでcatchする

static/js/main.js
  fetch('/api/pics', {
    mode: 'cors',
    method: 'POST',
    body: formData ,
  }).then(res => res.json()
  ).then(json => {
    if(json["status"] == "false"){
      alert(json["message"])
    }
    afterPost();
  }).catch(err => {
      alert("ファイルサイズが1MBを超えていませんか?")
      afterPost();
  })

image.png

とりあえず,アラートを出すことはできました.
応答が無いのは問題ですが…….

感想

fetchを用いたファイルの送信を理解することができました.しかし,上で述べましたエラーの原因については解明できなかったので,助言を頂ければ幸いです.

GitHub

Amakuchisan/pictures-warehouse

参考

How do I upload a file with the JS fetch API?
FlaskでAPI作って素のJavaScriptでAPI実行する

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした