これはなに
ここではSupabaseのRDBとstorageを使って、アップロードした画像の表示と、次回アクセス時にも画像を保持し、再びアップロードすれば更新できるようなフォームを作っていきます。
ここまで紹介しているような記事がなかなかなかったので、参考までに。
今回はNext.jsを使ったやり方を紹介します。API Routeも活用します。
Supabaseのセットアップ
今回は工程が多いので省きます。代わりにわかりやすく紹介している記事を紹介させていただきます。URL
とKEY
を忘れずに保持しましょう。NEXT_PUBLIC_
がないとクライアントでは取得できないのでつけておきます。
Nextの環境ができたらルートディレクトリにこの.env.local
をおいてください。
NEXT_PUBLIC_SUPABASE_URL = https://xxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_KEY = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
RDB
「img-editor」というtable名で進めていきます。urlを挿入できるようにcolumnを定義しておきます。本当はあった方がよいですが今回はRLSはつけてません。
Storage
「img-store」というbucket名で進めていきます。Public
にしておいてください。フォルダの作成は特に行いませんが、こちらはポリシーの設定が必要なので行っておきます。ここもテキトーです。「Policies」から...
「Enable read access to everyone」を選択した状態で、→「Use this template」
とりあえずこれでAPIを叩けばすぐ使える状態になります。
Supabaseのクライアントを作る
今回はaxios
を使ってAPIにアクセスします。今回はわかりやすいようにClassを作って、メソッドで処理しましょう。@supabase/supabase-js
が必要なのでnpmの場合はこのようにインストールし、import
してください。
$ npm install @supabase/supabase-js
import { createClient } from "@supabase/supabase-js";
export default class ImgEditor{
constructor(supabaseUrl, supabaseKey){
this.supabase = createClient(
supabaseUrl,
supabaseKey
);
};
// 画像のURLをTableから取得する
async getImageUrl(){
const { data, error } = await this.supabase
.from("img-editor")
.select("src");
if (error) {
throw error;
} else {
return data[0];
}
};
// 画像をアップロードする。成功したら画像のURLを返す
async uploadImage(fileObj){
const { data, error } = await this.supabase.storage
.from("img-store")
.upload(`upload-img.${fileObj.name.split(".")[1]}`, fileObj,
{ upsert: true }
);
if (error) {
throw error;
} else {
const src = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${data.fullPath}`
const { _, error } = await this.supabase
.from("img-editor")
.upsert({ id: 1, src: src });
if (error) {
throw error;
} else {
return { src: src };
}
}
};
};
ポイントなのは.upload(
upload-img.${fileObj.name.split(".")[1]}, fileObj,{ upsert: true });
の部分ですが、upload(保存先パス, fileObject, config)
という引数になっており、{upsert: true}
にすると上書き保存が許可されます。詳しくはこちら↓
Reactでフォームを作る
では、サンプルのフォームを作っていきます。今回は関数型コンポーネントで実装します。ハンドリングする箇所は一旦null
にしておきます。
export default function Index(){
return (
<>
<input type="file" accept="image/*" onChange={null}/>
<button onClick={null} disabled={null}>アップロード</button>
<p>↓アップロードした画像</p>
<img src={null} alt="selected-image" />
</>
)
};
選択画像のプレビュー
次に画像を選択するとimg
に表示されるようにします。useState
を使います。Next v13からクライアントコンポーネントには先頭にuse client
がないと動作しないのでつけ忘れに注意しましょう。
'use client'
import { useState } from "react";
import ImgEditor from "@/src/ImgEditor";
export default function Page(){
const [selectedImage, setImage] = useState();
return (
<>
<input
type="file"
accept="image/*"
onChange={(e) => setImage(e.currentTarget.files[0])}
/>
<button
onClick={null}
disabled={
selectedImage == null ||
typeof selectedImage == "string"
}
>
アップロード
</button>
<p>↓アップロードした画像</p>
<img
src={
selectedImage ?
typeof selectedImage == "string" ?
selectedImage :
window.URL.createObjectURL(selectedImage) :
null
}
alt="selected-image"
/>
</>
)
};
img
のプレビューではFileReader
をつかうというアプローチもありますが、window.URL.createObjectURL
を使う方が短く済むのでおすすめです。
ただし、null
が引数になった場合はエラーを吐くのでハンドリングします。
また、再アクセス時はアップロードされた画像のURLが入ることになるので、その場合もハンドリングしてやる必要があります。
初学者向けにまとめると、src
の部分をif
で書くとこうです。
if (selectedImage) {
if (typeof selectedImage == "string") {
return selectedImage;
} else {
return window.URL.createObjectURL(selectedImage);
}
} else {
return null;
}
画像のアップロード
アップロードされた画像のFileObject
をuseState
で管理「アップロード」ボタンのハンドリングで、作ったクラスメソッドのuploadImage
を呼んでみましょう。
'use client'
import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";
// Supabaseクライアント
const imgEditor = new ImgEditor(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
export default function Page(){
const [selectedImage, setImage] = useState();
// 画像アップロードのハンドリング
const saveImage = () =>{
imgEditor.uploadImage(selectedImage)
.then((data) => console.log(data))
.catch((error) => console.error(error));
};
return (
<>
<input
type="file"
accept="image/*"
onChange={(e) => setImage(e.currentTarget.files[0])}
/>
<button
onClick={saveImage}
disabled={
selectedImage == null ||
typeof selectedImage == "string"
}
>
アップロード
</button>
<p>↓アップロードした画像</p>
<img
src={
selectedImage ?
typeof selectedImage == "string" ?
selectedImage :
window.URL.createObjectURL(selectedImage) :
null
}
alt="selected-image"
/>
</>
)
};
これでアップロードされた画像はstorageおよびtableに保存されるようになったはずです。
ではテキトーな画像をアップロードしてみましょう。コンソールに{src: ...}
の形で返ってくれば成功です。
Supabase側で確認してもちゃんと入っているか確認しましょう。
画像のURLを取得する。
最後に、再アクセス時に前回アップロードした画像をテーブルから拾ってくるようにしましょう。
useEffect
とクラスメソッドのgetImgUrl
を使いましょう。
'use client'
import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";
const imgEditor = new ImgEditor(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
export default function Page(){
const [selectedImage, setImage] = useState();
// 初回レンダリング時に画像URLを取得する
useEffect(() => {
imgEditor.getImageUrl()
.then((data) => setImage(data.src))
.catch((error) => console.error(error));
}, []);
const saveImage = () =>{
imgEditor.uploadImage(selectedImage)
.then((data) => console.log(data))
.catch((error) => console.error(error));
};
return (
<>
<input
type="file"
accept="image/*"
onChange={(e) => setImage(e.currentTarget.files[0])}
/>
<button
onClick={saveImage}
disabled={
selectedImage == null ||
typeof selectedImage == "string"
}
>
アップロード
</button>
<p>↓アップロードした画像</p>
<img
src={
selectedImage ?
typeof selectedImage == "string" ?
selectedImage :
window.URL.createObjectURL(selectedImage) :
null
}
alt="selected-image"
/>
</>
)
};
これでページをリロードしても前回アップロードした画像が表示されたかと思います。
おわりに
今回は画像のアップロード、プレビュー、Supabaseへの保存、更新機能まで説明させていただきました。アカウント登録時のアバターやSNSなどの画像投稿機能なんかで使えそうですね。今回はパフォーマンスやセキュリティをガン無視しましたが、実際に使う場合はそこも意識して実装してみてはいかがでしょうか。