Vue と Firebase によるブログサービスの管理ページの例

記事
IT・テクノロジー

Vue と Firebase によるブログサービスの管理ページの例

Markdown で記事を書いて、Firebase を使ってブログサービスをする Web サービスを Vue で作成する場合の、管理ページの実施例を紹介します。この記事では、特に Markdown のメタデータを Firestore で管理する場合の例を紹介します。

投稿記事の一覧表示

投稿記事の一覧を表示する際のデータを Firebase のデータベースである、Firestore に保存してサービスを提供する場合、一覧の表示は、Firestore から取得するデータで実現可能です。 実際に、投稿の中身を表示する際は、Firebase のストレージのファイルからデータを読む必要がありますが、それ以外は、Firestore のデータだけで対応できます。

つまり、一般の利用者(ブログの読者)が利用する基本機能は二つという事になります。

* 投稿記事の一覧を見る
* 投稿記事の一覧から選択した記事の中身を見る
投稿記事の一覧の情報に、Firebase のストレージにあるファイルへのリンク(URL)を入れておけば、記事を選択した際に、リンク情報(URL)も同時に取得できるので、それを使ってファイルの中身を取得する処理があれば基本機能はカバーできます。

管理に必要な機能は?

では、サービスの管理者として必要な機能は、

* 記事の投稿
* 表示する記事の管理
* 投稿の削除
という事になります。記事の投稿は、Markdown で書かれたファイルを Firebase のストレージに保存する事と、Markdown のメタデータを抜き出して、Firestore に保存するこの二点になります。

表示する記事を管理する場合、投稿した記事のうちどの記事を表示するかを選べるというのは、あると便利な機能です。表示したくない記事の場合、投稿した記事そのものを削除してしまえば問題ありませんが、削除しなくても、記事を表示させるかさせないかを選べるとさらに便利です。

最後が、投稿した記事を完全に削除する機能です。 この機能は、不要な記事を完全に削除することで、Firebase のストレージの使用容量を減らす必要がある場合などに必要になります。基本的に、使用容量に問題がない場合、投稿した記事を「隠す」機能があれば、通常は Firebase ストレージのデータは残したままにした方が便利な場合が多くなります。

必要なメタデータ

ブログ記事のメタデータの例として、

* 記事のタイトル
* 記事の概要
* 記事の投稿日
などを Markdown に埋め込んでおくと便利です。単に記事のファイルの名前よりは、こうした記事に関連した詳細の情報を表示した方が利用者には便利だからです。

これに加えて、Firestore に保持していた方が便利な情報は、

* 表示、非表示の設定
* 最終更新日時
* ファイル名
* Fireabse ストレージのファイルの保存場所
などがあると便利です。

したがって、記事を投稿するための関数として、以下のような関数を実装しておけば、Firebase のストレージに投稿のファイルを保存するのと同時に、Firestore で保存するメタデータも一緒に保存できます。

export function uploadFile(path, file) {
  const fileRef = ref(storage, path + "/" + file.name);
  const uploadTask = uploadBytesResumable(fileRef, file);

  uploadTask.on(
    "state_changed",
    (snapshot) => {
      // Current completion in a percentage of the total transfer
      const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      switch (snapshot.state) {
        case "paused":
          console.log("Upload is paused.");
          break;
        case "running":
          console.log("Upload is running.");
          break;
      }
    },
    (error) => {
      // Error -- Handle unsuccessful uploads
      alert("ファイルのアップロードに失敗しました");
    },
    () => {
      // Handle successful uploads on complete
      // --> Gets the download URL
      getDownloadURL(uploadTask.snapshot.ref)
        .then((downloadURL) => {
          fetch(downloadURL).then((res) => {
            res.text().then(async (rawtext) => {
              const processed = matter(rawtext);
              const date = processed.data.date
                ? new Date(processed.data.date)
                : Date.now();
              const meta = {
                url: downloadURL,
                title: processed.data.title ? processed.data.title : "",
                path: path,
                name: file.name,
                description: processed.data.description
                  ? processed.data.description
                  : "",
                post_date: Math.floor(date.getTime() / 1000),
                update_date: Math.floor(new Date.now().getTime() / 1000),
                hide: false,
              };
              setMetadata(path, file.name, meta).then((result) => {
                if (result) {
                  // Operation is successful to add the database entry
                  alert("ファイルをアップロードしました");
                } else {
                  // Operation failed to add the database entry
                  alert("ファイルのアップロードに失敗しました");
                }
              });
            });
          });
        })
        .catch((error) => {
          alert("ファイルのアップロードに失敗しました");
        });
    }
  );
}
このようなデータを Firestore に保存します。 メタデータの JSON は、

{
  "url": "投稿のファイルへのリンク(URL)",
  "title": "投稿のタイトル",
  "path": "Firebaseストレージのファイルの保存場所",
  "name": "ファイルの名前",
  "description": "投稿の概要",
  "post_date": 1648684800,
  "update_date": 1648684800,
  "hide": false // 投稿の表示(false)/投稿の非表示(true)
}
あとは、投稿の一覧を一般利用者(ブログの読者)向けに表示する際は、「hide」が「false」のデータだけを表示するようにすると、実際に Firebase ストレージに保存されているファイルを削除しなくても、利用者がその投稿のデータを見えなくする事が可能になります。

管理者側の機能として必要なもの

一方で管理者側の機能としては、

* 記事の投稿
* 記事の一覧表示(fires とれのメタデータを使った一覧)
* Firebase ストレージに保存されているファイルの一覧
* Firestore のメタデータの更新
* Firestore のメタデータの削除
* Firebase ストレージのファイルの削除
* Firebase ストレージのファイルからメターデータを取り出し、Firestore に追加
が必要になります。

これらの機能は、これまで紹介してきた、Firebase の処理で可能です。 Firestore のドキュメントの更新には「setDoc()」が利用できます。Firebase のドキュメントの削除には、 「deleteDoc()」を利用すれば簡単に実現できます。

管理機能の実装で注意が必要なのは、Firebase のストレージのファイルを削除した場合です。この場合、投稿の記事そのものが削除されるので、同時に、Firestore のメタデータのドキュメントも一緒に削除する必要があります。つまり、記事のファイルがないので、記事の中身を表示することが不可能になるため、メタデータ自体も削除する必要があるという事です。

また、逆に Firebase のストレージにファイルは残っているけれど、Firestore のデータが削除されている場合には、Firebase のストレージのファイルを読み取って、Firestore のメタデータを Firestore に登録するとう処理も必要になります。

細かい事ですが、Firebase のストレージの投稿の記事の Markdown と、Firestore のメタデータの整合性が取れていないと、サービスがきちんと機能しなくなるので、こうした連携が壊れないように考慮して実装することがとても大切になります。

よく考える必要があるのは、Firestore のメタデータの削除と、Firebase ストレージのファイルの削除では、同じような削除でも、意味が違っています。

Firebase のストレージのファイルを削除する例です。

deleteFile(ref) {
      const filename = ref.name;
      const my_query = query(
        collection(db, "sample"),
        where("name", "==", filename)
      );
      getDocs(my_query).then((querySnapshot) => {
        if (querySnapshot.size === 1) {
          const doc = querySnapshot.docs[0];
          DeleteMetadata("sample", doc.id).then((result) => {
            if (result) {
              deleteFileFromStorage(ref).then((success) => {
                if (success) {
                  alert("ファイルを消去しました");
                } else {
                  alert("ファイルの消去に失敗しました");
                }
              });
            }
          });
        }
      });
},
先に、Firebase のストレージのファイルを削除してしまうと、データベースにはメタデータが残っていますので、もし、削除されたファイルが一覧から選択された場合にはエラーになってしまいます。 そこで、先に Firestore のメタデータを削除した上で、最後に Firebase ストレージのファイルを削除する方が安全な手順になります。

import { deleteObject } from "firebase/storage";
import { deleteDoc, doc } from "firebase/firestore";
export function deleteFileFromStorage(ref) {
  return new Promise((resolve) => {
    deleteObject(ref)
      .then(() => {
        resolve(true);
      })
      .catch((error) => {
        resolve(false);
      });
  });
}

export function DeleteMetadata(target_collection, doc_id) {
  return new Promise((resolve) => {
    deleteDoc(doc(db, target_collection, doc_id))
      .then(() => {
        // Successful to delete the data
        resolve(true);
      })
      .catch((error) => {
        // Failed to delete the data
        resolve(false);
      });
  });
}
以上が削除のための、Firebase 関連の処理関数の例です。

まとめ
今回は、ブログサービスの管理という視点で実際の実装上のポイントを紹介しました。 同じ削除でも、大元の情報を削除するのと、大元の情報から取得した情報を削除するのでは、意味が大きく違います。削除の処理は、考えようによっては「危険な処理」で削除してしまうと、実際には元に戻すのが難しいケースが多くなります。そういう観点では、大元のデータは消さずに「隠す」というやり方でサービスを実装すると、誤操作によるデータの消失を最小限にできます。

最近の OS ではファイルの削除の際に、削除したファイルを一旦「ゴミ箱」にうつした上で、削除するようになっています。こうすることで、誤ってファイルを消してしまった場合でも、復旧する仕組みを提供しています。 Web サービスでも同じように考えると、利用者や管理者により使いやすいサービスになります。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す