はじめに
現在React
とDjango REST framework
を用いて成果物を制作しています。
画像アップロード機能を実装するまでに、多少苦労をしたので、他の学習者の方がより簡単に実装できるように一連の流れをまとめておきたいと思って執筆させていただいております。
画像アップロード機能は詰まることが多いと聞いたことがあるので、この記事が少しでもお役に立てれば幸いです。
参考
参考にした記事は以下のものです。
バックエンドのdjango
側の画像アップロード機能実装のためと、フロントエンドの画像プレピュー機能実装のために参照をしました。
はじめてのDjango (7) 画像データの管理やページへの表示,アップロードの方法などについて知ろう
Reactで超簡単な画像ビューアを作る - FileReader
バックエンド
まず、バックエンドから簡潔に説明していきたいと思います。
画像を扱うための、Pillow
というパッケージを仮想環境にダウンロードをしなければいけません。
なので、まずはじめに仮想環境をアクティベートした後に以下のコードを打ってダウンロードしてください。
pip install pillow
models.py
続いて、画像を扱うモデルを作成していきます。今回は私が実際に使ったモデルを用いて説明を進めていきます。
画像を扱うためには、ImageField
を設定しなければいけません。
class Item_Image(models.Model):
image = models.ImageField(upload_to="images/")
item = models.ForeignKey(
Give_Item, on_delete=models.CASCADE, related_name="item_image")
def __str__(self):
return self.item.parent_item.name
class Meta:
db_table = "item_images"
upload_to
というのは、settings.py
で設定したMEDIA_ROOT
からの相対パスを示しています。画像はデータベースで直接保存されるわけではなく、この指定したディレクトリにアップロードされているというのが実際のロジックです。
特にモデルでは書くことはないので、このままsettings.py
の説明に移ります。
settings.py
追記する内容は以下の通りです。
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
__file__
は実行中のファイル(ここではsettings.py
)を参照していて、os.path.dirname
は簡単に言うと一個上のディレクトリを参照するので、BASE_DIR
はプロジェクトやアプリを全て格納しているフォルダ名を参照していることになります。
これによって、media
ディレクトリのパスを作成することができました。
続いて、プロジェクトの方のurls.py
に移ります。
urls.py
記述はとても簡単で、一種のおまじないみたいなものです。
import
を忘れないようにしてください。
from django.conf import settings # New
from django.contrib.staticfiles.urls import static # New
from django.contrib.staticfiles.urls import staticfiles_urlpatterns # New
urlpatterns += staticfiles_urlpatterns() # New
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # New
urlpatterns
に関しては、他のエンドポイントを書いた後の下に書いて大丈夫です。具体的には以下のようになります。
urlpatterns = [
path("api/", include("app.urls")),
path('admin/', admin.site.urls),
url('rest-auth/', include('rest_auth.urls')),
url('rest-auth/registration/', include('rest_auth.registration.urls'))
]
# これで準備完了です
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
あとは、React
を使うならDjango REST framework
も必要となるのserializers.py
, views.py
も設定しておきましょう。
serializers.py
, views.py
ここは特に画像アップロードで加筆することはありません。
models.py
を作成しているなら、ModelSerializer
,ModelViewSet
を使えば楽にAPI実装ができます。
class Item_ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Item_Image
fields = "__all__"
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
permission_classes = [
permissions.AllowAny
]
serializer_class = CommentSerializer
これにてバックエンドの設定は完了です。
フロントエンドに移ります。
フロントエンド
input
フォームの作成
画像をアップロードしてもらうことになるので、input
を作成する必要があります。
ここで大切なのは**form
タグを使用することです。**
button
でonSubmit
に関数を代入した方がe.preventDefault()
を書かなくて楽だと思われるかもしれませんが、画像アップロードの際に必要になるので、必ずform
タグで囲ってあげてください。
ちなみに、今回は複数投稿での実装となります。
<div className="imageForm">
<form onSubmit={this.handleSubmit}>
//
省略
//
<label>商品画像</label>
//
// 複数アップロードする際は、multipleをつける必要があります
//
<input type="file" multiple onChange={this.handleImageSelect} />
//
// 下記はプレビュー機能のためのコードなので後で説明を加えます
//
{this.state.imgUrls.length === 0
? null
: this.state.imgUrls.map((img, idx) => {
return <img key={idx} src={img}></img>;
})}
<form/>
プレビュー機能の実装
先にコードを書きます。
readImageUrl = () => {
const files = Array.from(this.state.info.images);
Promise.all(
files.map((file) => {
//
// 3
//
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', (event) => {
resolve(event.target.result);
});
reader.addEventListener('error', reject);
reader.readAsDataURL(file);
});
})
)
.then((images) => {
this.setState({ imgUrls: images });
})
.catch((err) => console.log(err));
};
handleImageSelect = async (e) => {
//
// 1
//
await this.setState({
info: { ...this.state.info, images: [...this.state.info.images, ...e.target.files] },
});
this.setState({
message: {...this.state.message, images: this.validator("images", this.state.info.images )}
})
//
// 2
//
this.readImageUrl();
};
順番に説明をしていきます。
私のコードのValidationの実装方法に関連して、少し記述がごちゃごちゃになっております。申し訳ございません。
1.state
内に選択されたファイルを格納する
一番はじめに行うことは極めて単純です。選択されたファイルをSubmitするためにstate
に入れ込むだけです。私の場合、後のValidationの都合でasycn/await
で全ファイルが代入されるまで待っていますが、この非同期処理への対応は必須ではないのでお任せします。(後にわかったことですがsetState
をasync/await
に付けても特に効果はないようです。)
重要なことは、選択されたファイルをスプレッド構文を用いて、配列にまとめて代入するということです。
2. ファイルをインラインで埋め込むdata:URL
に変更する
(この表現が正しいかはわかりませんが)inputから得られるFile
はBlob
を継承しているため中のデータに直接アクセスすることはできません。File
に格納されたデータにアクセスするための一つの方法が**File
をdata:URL"として読み込むこと**であるので、この関数を使って配列内の
File`達を変換しているということになります。
3. FileReader
を使って画像のURLを取得しstate
に入れる
このプレビュー機能の実装方法において、肝となるのはFileReader
というオブジェクトです。今回は複数投稿での実装ということで、input
から得たfiles
arrayをmap
しています。書かれてる順番が前後しますが、Promise
の中で行われているのは、まずreadAsDataURL
メソッドを使って選択されたファイルを読み込むことです。名の通り読み込まれたファイルは上述のdata:URL
に変換されます。そして、読み込みが終わったと同時に発火するのが、その上のload
イベントです。ちなみにこれはaddEventListener
を使う必要はなく、onload
というプロパティを使ってより簡潔に記述することもできます。書かれてる順がややこしくさせますが、このload
イベントは読み込みが完了され他あと、result
として読み込まれたファイルを返してくれます。今回はプレビューとして画像を描画したいので、state
に入れます。
{
this.state.imgUrls.length === 0
? null
: this.state.imgUrls.map((img, idx) => {
return <img key={idx} src={img} alc="アップロード写真" height="150px"></img>;
})
}
今回は複数投稿なので、先ほど変換されたdata:URL
が入ったファイルをmap
します。後は、img
タグのsrc
に受け取ったURL
を入れるだけです。
画像アップロードの実装
this.state.info.images.map((image) => {
let data = new FormData();
data.append('image', image);
data.append('item', giveItem_id);
axios
.post(this.props.axiosUrl + 'image/', data, authHeader)
.then((res) => console.log(res.data))
.catch((err) => console.log(err));
});
画像アップロードに関連するコードだけ抜粋して書いていますが、説明に大きく影響はないのでそのまま使用させていただきます。
React
, Django Rest Framework
において画像をアップロードする肝となるのはFormData
です。他のCharField
,IntegerField
モデルでは可能なaxios
のdata
部分に値を入れてPOSTリクエストを送ってもエラーが返ってきます。ImageField
はFileField
を継承しているからか理由は定かではありませんが、少なくとも画像アップロードにはFormData
オブジェクトとしてリクエストを送らないとモデルを作成できないのは間違い無いと思います。
FormData
を使うことさえわかれば実装は至極単純です。.append(name, value)
を用いてFormData()
にリクエストを送りたい値を入れるだけです。
私の場合、複数投稿された画像一枚ごとにモデルを新規作成したかったのでinput
から得たfile
を格納した配列をmap
しています。
豆知識となりますが、FormData
にちゃんと値が入っているか確認したい場合は以下の方法を使えば可能です(上記のコードからlet data = new FormData()
として代入されている前提です)。
console.log(...data)
まとめ
上記が私が実装した方法です。
input
から得られるfile
はBlob
形式であるBlob
形式のファイルをdata:URL
等に変換するのにFileReader()
が有効である- フロントからバックエンドへの
ImageField
を持つモデルを作る場合、FormData
として送信しなければならない
以上がまとめです。
他にもより良い実装方法があると思いますが、あくまでも一つの方法として参考にしていただければ幸いです。
アドバイスや間違っている点含めコメントをいただければとても嬉しいです。
拙く読み辛い文章だったとは思いますが最後までご覧いただき誠にありがとうございました。