Closed26

Firebase v9 Storage x Next.js: 画像をアップロードする

CaaaaatsCaaaaats

拡張子か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

CaaaaatsCaaaaats

そもそも ​inputタグのonchangeで渡されたファイル名をそのままimgタグのsrcに投げてあげれば表示されると思っていたけど、セキュリティの問題から前は大丈夫だったけど今はダメらしい
この記事が面白い
https://blog.ver001.com/javascript_preview_canvas/

CaaaaatsCaaaaats

imgタグのsrcにはbase64データが埋め込めるんだよ

<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" >

更にJavaScriptにFileAPIが実装され、そのFileReaderオブジェクトでファイルを読み込むとbase64エンコーディングされたData URIが取得できます。ということは、それをそのままimgタグのsrcにセットしてやるだけでプレビューができちゃう。

fileReader.readAsDataURL(obj.files[0]);
CaaaaatsCaaaaats
  1. FileReaderをロード
  2. 選択された画像ファイルをreadAsDataURLで読み込み
  3. 読み込み完了したらimgタグのsrcにセット

あ。これだけ?ならライブラリ使わなくても良くね・・・

CaaaaatsCaaaaats

参考にしながらこんなふうに実装したけどエラーが出た

   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);
    };
  };
CaaaaatsCaaaaats

どうやら/で始まるか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`
        )
      }
    }
  }

https://github.com/vercel/next.js/blob/canary/packages/next/client/image.tsx

CaaaaatsCaaaaats

ってことでuse-file-uploadのライブラリ使うことにしたら型エラーが出た。
どうやら最新のReactバージョンupdateでそれにライブラリの型指定がついていけてないらしい。
とりあえず、このやり方でやろう
https://github.com/Marvinified/use-file-upload/issues/2

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>,
    ]
}

CaaaaatsCaaaaats

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)
        });
      },
    );
  };
CaaaaatsCaaaaats

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>
)

CaaaaatsCaaaaats

おそらく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

CaaaaatsCaaaaats

参考にしたレポジトリは大体どれもimgタグを使ってたけど、それだとnext.jsではエラーが出る。(Imageコンポーネントに置き換えられているため)

唯一参考になるレポジトリは、やはり

var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => {
      setSrc(reader.result as string);
    };

みたいな感じでreadAdDataURL()メソッドを用いてそれをImageコンポーネントのsrcにセットしている感じだった。
https://github.com/stefan-dimitrovski/FacebookClone-ReactApp/blob/main/components/InputBox.js

CaaaaatsCaaaaats

公式でも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
  });

CaaaaatsCaaaaats

たぶん、自分の場合はいったんstateに値を保持しているから?それが原因かもしれない。

CaaaaatsCaaaaats

次のアクションとしてはstateに値を持たせずに直接setする実装方法を試そう

CaaaaatsCaaaaats

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 })
                        })
                    })

CaaaaatsCaaaaats

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=
の部分だけを消したいどうやってやるんだろう。

CaaaaatsCaaaaats

imgタグ使ったらできた・・・涙

<img
    className='rounded-full cursor-pointer'
    src={`${avatarImage}`}
    alt='Avatar Image'
    width={100}
    height={100}
/>
CaaaaatsCaaaaats

ただこのやり方だと、一旦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} />}
・・・

参考
https://zenn.dev/fujiyama/articles/50b0a73acd89b7

このスクラップは2021/11/19にクローズされました