Firebase v9 Storage x Next.js: 画像をアップロードする
Image uploader作るだけで思ったよりコード量が多いけど、色々勉強になりそう。
reactのライブラリでもサクッとできる
完成重視なのでひとまずこちらにしてみようかな
拡張子かMIMEタイプでアップロードのタイプを制限できるみたいだけど、inputのaccept属性で指定してるやつとどっちが優先適用される?試してみるか
Setting Allowed File type
Restrict what types of files can be selected using the accept option.It Support all file extensions or MIME types
そもそも inputタグのonchangeで渡されたファイル名をそのままimgタグのsrcに投げてあげれば表示されると思っていたけど、セキュリティの問題から前は大丈夫だったけど今はダメらしい
この記事が面白い
imgタグのsrcにはbase64データが埋め込めるんだよ
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" >
更にJavaScriptにFileAPIが実装され、そのFileReaderオブジェクトでファイルを読み込むとbase64エンコーディングされたData URIが取得できます。ということは、それをそのままimgタグのsrcにセットしてやるだけでプレビューができちゃう。
fileReader.readAsDataURL(obj.files[0]);
- FileReaderをロード
- 選択された画像ファイルをreadAsDataURLで読み込み
- 読み込み完了したらimgタグのsrcにセット
あ。これだけ?ならライブラリ使わなくても良くね・・・
参考にしながらこんなふうに実装したけどエラーが出た
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
let uploadedFile = e.target.files![0];
let fileReader = new FileReader().readAsDataURL(uploadedFile);
setAvatarImage(fileReader); //[0]なくてもいい? 指定の仕方を考え直す必要あるかも
console.log(avatarImage);
};
エラー内容
Uncaught Error: Failed to parse src "undefined" on `next/image`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)
undifindedになっているから読み込めていない
正しい書き方はこれらしいので、あとでかえる
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
setSrc(reader.result as string);
};
};
どうやら/で始まるかURLの絶対パスで指定しないとダメらしい。
公式レポ見てみたらよくわかった。
function defaultLoader({
root,
src,
width,
quality,
}: DefaultImageLoaderProps): string {
if (process.env.NODE_ENV !== 'production') {
const missingValues = []
// these should always be provided but make sure they are
if (!src) missingValues.push('src')
if (!width) missingValues.push('width')
if (missingValues.length > 0) {
throw new Error(
`Next Image Optimization requires ${missingValues.join(
', '
)} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
{ src, width, quality }
)}`
)
}
if (src.startsWith('//')) {
throw new Error(
`Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)`
)
}
if (!src.startsWith('/') && configDomains) {
let parsedSrc: URL
try {
parsedSrc = new URL(src)
} catch (err) {
console.error(err)
throw new Error(
`Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)`
)
}
if (
process.env.NODE_ENV !== 'test' &&
!configDomains.includes(parsedSrc.hostname)
) {
throw new Error(
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
)
}
}
}
ってことでuse-file-uploadのライブラリ使うことにしたら型エラーが出た。
どうやら最新のReactバージョンupdateでそれにライブラリの型指定がついていけてないらしい。
とりあえず、このやり方でやろう
declare module "use-file-upload" {
type FileUpload = {
source: URL;
name: string;
size: number;
file: File;
}
type Callback = (file: FileUpload | [FileUpload]) => void;
const useFileUpload: () => [
file<FileUpload | [FileUpload]>,
selectFiles<({ accept: string, multiple: boolean }, callback: Callback) => void>,
]
}
このレポジトリ参考になりそう
preveiwがうまくいかないのでひとまず、storageに画像アップロードしてそのURLをImageタグのsrcにインプットするようにする
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files![0];
// setAvatarImage(file);
uploadFiles(file)
};
const uploadFiles = (file: any) => {
if (!file) return;
const storageRef = ref(storage, `/images/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
'state_changed',
(snapshot) => {
const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
setProgress(progress);
},
(err) => console.log(err),
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
console.log(url)
setAvatarImage(url)
});
},
);
};
View側
return (
<button
className='text-gray-400 transition-all duration-500 ml-6 hover:text-penn-gray outline-none focus:ring-2 focus:ring-penn-green focus:ring-opacity-30 rounded-md'
onClick={handleIsNext}
type='submit'
>
← Go back
</button>
<div className='text-center'>
<h1 className='text-4xl my-4 font-bold text-penn-dark cursor-default'>
Setup your profile
</h1>
<Image
className='rounded-full cursor-pointer'
// src='/avatar.png'
src={`${avatarImage}`}
// src={files?.source.replace(/^...../g, '') || avatarImage} //replace(/^...../g, '')先頭の5文字を空文字に置換(blob:が邪魔なので。。。)
alt='Avatar Image'
width={100}
height={100}
layout='fixed'
/>
<div>
<button
className='text-sm mb-4 text-gray-400 rounded cursor-pointer focus:outline-none focus:ring-penn-green focus:ring-2 focus:ring-opacity-50'
onClick={handleFileClick}
>
変更する
</button>
<input
className='hidden'
type='file'
ref={hiddenFileInput}
onChange={handleFileChange}
accept='image/*'
/>
</div>
)
おそらくURLの取得方法が直接的なのかnext/imageに跳ね返される
エラー内容
http://localhost:3000/_next/image?url=https://firebasestorage.googleapis.com/v0/b/penn-67af2.appspot.com/o/images%2Fnouser-icon.png?alt=media&token=4c055f3c-700e-4ca6-9e3c-03dc9b0979c2&w=256&q=75
参考にしたレポジトリは大体どれもimgタグを使ってたけど、それだとnext.jsではエラーが出る。(Imageコンポーネントに置き換えられているため)
唯一参考になるレポジトリは、やはり
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
setSrc(reader.result as string);
};
みたいな感じでreadAdDataURL()
メソッドを用いてそれをImageコンポーネントのsrcにセットしている感じだった。
公式でもgetDownLoadURLで取得/エンコードしたurlをimgタグのsrcに挿入している
import { getStorage, ref, getDownloadURL } from "firebase/storage";
const storage = getStorage();
getDownloadURL(ref(storage, 'images/stars.jpg'))
.then((url) => {
// `url` is the download URL for 'images/stars.jpg'
// This can be downloaded directly:
const xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onload = (event) => {
const blob = xhr.response;
};
xhr.open('GET', url);
xhr.send();
// Or inserted into an <img> element
const img = document.getElementById('myimg');
img.setAttribute('src', url);
})
.catch((error) => {
// Handle any errors
});
たぶん、自分の場合はいったんstateに値を保持しているから?それが原因かもしれない。
次のアクションとしてはstateに値を持たせずに直接setする実装方法を試そう
e.g.
uploadTask.on('state_change', null, error => console.error(error), () =>{
storage.ref('posts').child(doc.id).getDownloadURL().then(url => {
db.collection('posts').doc(doc.id).set({
postImage: url
}, { merge: true })
})
})
http://localhost:3000/_next/image?url=https://firebasestorage.googleapis.com/v0/b/penn-67af2.appspot.com/o/images%2Fnouser-icon.png?alt=media&token=4c055f3c-700e-4ca6-9e3c-03dc9b0979c2&w=256&q=75
の
localhost:3000/_next/image?url=
の部分だけを消したいどうやってやるんだろう。
imgタグ使ったらできた・・・涙
<img
className='rounded-full cursor-pointer'
src={`${avatarImage}`}
alt='Avatar Image'
width={100}
height={100}
/>
ただこのやり方だと、一旦storageを経由するので処理が多いかも?
今のところ表示速度に問題は特にないけど、問題ありそうだったらsrcセットするurlを以下の書き方に変える。
const [src, setSrc] = useState('');
const handlePreview = (files: any) => {
if (files === null) {
return;
}
const file = files[0];
if (file === null) {
return;
}
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
setSrc(reader.result as string);
};
};
return
・・・
{src && <img src={src} />}
・・・
参考
この動画は最強だ
BUG報告されててまだダメみたいっすねー