Vue ✖️ Firebase Storageで画像保存

# はじめに
Vue.jsとFirebaseの導入については、省いております．
ご了承ください．

# 画像投稿機能 Vue.jsのコード

### プレビュー付き画像フィールド


``` VImgField.vue
<template>
  <div>
    <v-row v-if="uploadedImage" justify="center" align="center">
      <v-sheet width="300">
        <v-img :src="uploadedImage" />
      </v-sheet>
    </v-row>
    <div v-cloak @drop.prevent="addDropFile" @dragover.prevent>
      <v-row justify="center" align="center">
        <v-col cols="11" sm="8" md="6">
          <v-file-input
            v-model="file"
            accept="image/png, image/jpeg, image/jpg, image/bmp"
            prepend-icon="mdi-camera"
            placeholder="画像ファイル(png, jpeg, jpg, bmp)を選択"
          />
        </v-col>
      </v-row>
    </div>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  SetupContext,
  ref,
  watch,
} from '@vue/composition-api';

// アップロードを許可する拡張子
const allowExts: string[] = ['jpg', 'jpeg', 'png', 'bmp'];

// ファイル名から拡張子を取得する関数
const getExt = (filename: string): string => {
  const pos = filename.lastIndexOf('.');
  if (pos === -1) {
    return '';
  }
  return filename.slice(pos + 1);
};

// ファイルが許可されている拡張子か確認する関数
const checkExt = (file: File | undefined): boolean => {
  if (file) {
    const ext = getExt(file.name).toLowerCase();
    if (allowExts.indexOf(ext) === -1) {
      return false;
    }
  }
  return true;
};

// 画像ファイルをBase64に変換
const getBase64 = (file: File): Promise<string | ArrayBuffer | null> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = (error) => reject(error);
  });
};

// Composition API
export default defineComponent({
  setup(props, context: SetupContext) {
    const uploadedImage = ref<string | ArrayBuffer | null>(null);
    const file = ref<File | undefined>(undefined);

    // ファイルがドラッグで追加されたときのメソッド
    const addDropFile = (e: DragEvent) => {
      const dt: DataTransfer | null = e.dataTransfer;
      if (dt) {
        file.value = dt.files[0];
      }
    };

    // ファイルの変更を監視し
    // 変更があれば、Base64に変換しviewを表示
    watch(file, (newFile, oldFile) => {
      if (checkExt(newFile)) {
        if (newFile) {
          getBase64(newFile).then((image: string | ArrayBuffer | null) => {
            uploadedImage.value = image;
            context.emit('change-file', file.value);
          });
        } else {
          uploadedImage.value = null;
        }
      } else {
        file.value = oldFile;
        context.emit('error-occurred', 'ファイル形式が正しくありません');
      }
    });

    return {
      uploadedImage,
      file,
      addDropFile,
    };
  },
});
</script>

<style scoped>
label > input {
  display: none;
}

label {
  padding: 0 1rem;
  border: solid 1px #888;
}

label::after {
  content: '+';
  font-size: 1rem;
  color: #888;
  padding-left: 1rem;
}
</style>

```

### 画像送信フォーム

```VPostForm.vue
<template>
  <v-row>
    <v-col cols="11" sm="8">
      <v-alert v-show="error" type="error">{{ error }}</v-alert>
    </v-col>
    <v-col cols="11">
      <a :href="url" target="_blank">{{ url }}</a>
    </v-col>
    <v-col cols="11">
      <v-img-field
        @change-file="changeFile"
        @error-occurred="catchError"
      />
    </v-col>
    <v-col cols="10">
      <v-btn @click="putImage">
        Post
        <v-icon>
          mdi-telegram
        </v-icon>
      </v-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import Vue from 'vue';
import VImgField from '@/components/VImgField.vue';
// Firebaseにアップロードするモジュール
import Uploader from '@/utils/uploader';

export default Vue.extend({
  name: 'VPostForm',
  components: {
    VImgField
  },
  data: () => ({
    file: {} as File,
    error: '',
    url: ''
  }),
  methods: {
    changeFile(uploadedFile: File) {
      this.file = uploadedFile
    },
    catchError(msg: string) {
      this.error = msg
    },
    // アップロード
    async putImage() {
      this.isLoading = true;
      try {
        this.url = await Uploader.put(this.file);
      } catch (err) {
        this.error = err;
      }
      this.isLoading = false;
    },
  }
})
</script>
```

これでVueでView機能付きのフォームを作ることができました．

## 少し脱線
今回のフォームは、Composition APIとOptions APIの両方を使用して、作っています．
理由として、私は、メソッドが多く複雑なコンポーネントは、Composition APIを使用して、単純なコンポーネントはOptions APIを使用するみたいな使い分けをしています．

# Firebase側の実装

## Firebaseのsetting

```@/plugins/firebase.ts
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/storage';

const config = {
  apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
  authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.VUE_APP_FIREBASE_DATABASE_URL,
  projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_PROJECT,
  messagingSenderId: process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.VUE_APP_FIREBASE_APP_ID,
  measurementId: process.env.VUE_APP_FIREBASE_MEASUREMENT_ID,
};

firebase.initializeApp(config);

export default firebase;
```

## アップロードをする関数

```@/utils/uploader.ts
import firebase from '@/plugins/firebase'
const storageRef = firebase.storage().ref();

export default {
  put(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      // ファイルのパスを生成
      const mountainsRef = storageRef.child(`insects/${file.name}`);

      // ファイルをアップロード
      mountainsRef
        .put(file)
        .then((snapshot) => {
          // アップロードしたファイルのURLを取得
          snapshot.ref
            .getDownloadURL()
            .then((downloadURL: string) => {
              resolve(downloadURL);
            })
            .catch((error) => {
              reject(error.message);
            });
        })
        .catch((error) => {
          reject(error.message);
        });
    });
  },
}
```

これで実装できます