概要
本記事は、【JAMstack】Next.js × Firebase で JAMstack なブログサイトを作る③[Hooks]の続きとなっています。
本記事では、Firebase Storageへ記事の投稿・削除を行うアプリを作成します。
JAMstackブログサービスの基幹部分は前回までで完成しましたが、記事の投稿・削除はFirebase Storage上でしかできないので、ブログの管理者用のアプリを作成します。
プロジェクトの作成
Firebase Hosting
Firebase Hostingのプロジェクトを作成しておきます。
※ Firebase自体のプロジェクトは、【JAMstack】Next.js × Firebase で JAMstack なブログサイトを作る②[ブログサイト]で作成したものを使います。
アプリケーション
アプリケーションは、RCA(create-react-app)で作成します。
作業フォルダを作成して、そこでコマンドプロンプトで以下を実行します。
npx create-react-app . --template typescript
以下を実行して動作確認をします。
npm run start
Hosting テスト
アプリケーションを作成したら、Hostingができるか確認しておきます。
firebase-toolsをインストールしていない場合は、インストールします。
npm i -g firebase-tools
プロジェクトで、コマンドプロンプトを使用して以下を実行します。
firebase init
初期設定は、以下のようにします。
- Hosting(上)を選択します。
- 既存プロジェクトとして、Firebaseのプロジェクトを選択します。
- デプロイするフォルダ名をbuildにします。
- React(SPA)なので、single-page-appをYesにします。
- GitHubと連携してリビルドする設定はNoにしておきます。(お好み)
初期設定ができたら、以下を実行してHostingします。
npm run deploy
成功すればURLが表示されるので、アクセスしてReactの初期ページが表示されればOKです。
アプリケーションの作成
機能
メインの機能として、以下のことをできるようにします。
- Firebase Storageにブログ記事を投稿する
- Firebase Storage内のブログ記事を削除する
- 削除するために、Firebase Storage内のブログ記事を表示する
オプションの機能として、以下も実装します。
- Firebase Authenticationを使用して認証機能を実装する
- ブログ記事で画像を扱えるようにするために、画像投稿機能を実装する
- 画像投稿機能を実装する上で、削除、一覧の表示機能も実装する
使用するライブラリ
UIは、Material-UIを使用します。
状態管理は、Recoilを使用します。(使うのは認証部分のみ)
スタイリングは、Material-UIのコンポーネントはMaterial-UIで用意されているStyle方法、通常のコンポーネントについてはemotion/cssで行います。
パッケージ インストール
npm i @material-ui/core @material-ui/icons
npm i recoil @types/recoil
npm i @emotion/css
npm i gray-matter
gray-matterは、取得した記事をテキストデータからオブジェクトデータに変換するために使用します。
Firebase Storage のアクセス設定
Rules
Firebase Storageの設定で、Rulesを変更します。
CORS
アプリケーション内で記事や画像を取得する場合、StorageにCORSを設定する必要があります。
設定方法は以下を確認してください。
外部ユーザーの Storage アクセス権限
画像をブログ記事に埋め込んで表示するためには、Storageを外部に公開する必要があります。
特殊なメンバーallUsersを追加して、Storageの読み取り専用の権限を与えます。
コーディングについて
コーディング部分は、長くなるので割愛します。成果物のGitHubリンクを参照してください。
環境変数の設定
RCA(create-react-app)で作成されたプロジェクトでは、特になにかをインストールしなくても環境変数を扱えます。また、環境変数の接頭辞はREACT_APP_にする必要があります。
root直下に.env.local
を作成して、FirebaseのSDK の設定と構成で取得できる値を以下のように設定します。
REACT_APP_FIREBASE_APIKEY="apiKey"
REACT_APP_FIREBASE_DOMAIN="authDomain"
REACT_APP_FIREBASE_DATABASE="https://PROJECT_ID.firebaseio.com"
REACT_APP_FIREBASE_PROJECT_ID="projectId"
REACT_APP_FIREBASE_STORAGE_BUCKET="storageBucket"
REACT_APP_FIREBASE_SENDER_ID="messagingSenderId"
REACT_APP_FIREBASE_APP_ID="appId"
以下のファイルを作成して、firebaseの初期化設定を行う処理を記述します。
import 'firebase/auth';
import 'firebase/storage';
import firebase from 'firebase/app';
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID
};
if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);
export const auth = firebase.auth();
export const storage = firebase.storage();
export const provider = new firebase.auth.GoogleAuthProvider();
認証では、メール&パスワードの他にGoogleアカウント認証もできるようにしました。
SingIn 状態の監視
Firebase Authenticationには、ユーザーのSingnIn状態を監視する関数があります。
これを状態管理ライブラリ(Recoil)と組み合わせることで、SignIn機能をスムーズに実装できます。
/**
* SignInの状態を監視する
*/
export const useAuth = () => {
const [signInUser, setSignInUser] = useRecoilState(signInUserState);
const resetStatus = useResetRecoilState(signInUserState);
useEffect(() => {
const unSub = auth.onAuthStateChanged(authUser => {
if (authUser) {
setSignInUser({
uid: authUser.uid,
email: authUser.email!
});
} else {
resetStatus();
}
});
return () => unSub();
}, [setSignInUser, resetStatus]);
return signInUser;
};
記事の取得
記事は、getDownloadURLを使用してurlを取得して、fetchすることで取得できます。
それをgray-matterを使用して解析して、必要な情報を抜き出します。
const getArticleContents = async (article: any) => {
const url = await article.getDownloadURL();
const res = await fetch(url);
const data = await res.text();
const matterResult = matter(data);
return {
path: article.fullPath as string,
...(matterResult.data as { date: string; title: string })
};
};
※ 引数の型anyは、本来firebase.storage.Reference
ですが、正常に設定できなかったため泣く泣くanyにしています。
画像の取得
Firestore Storageを外部公開(読み取りのみ)しているので、画像はそれを利用してurlを取得し、imgタグに割り当てます。
const urlPrefix = `https://storage.googleapis.com/${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}/`;
/**
* 画像データ配列を取得する
* @returns 日付ソートした画像データ配列
*/
export const getImages = async () => {
try {
const result: ImageType[] = [];
const images = await storage.ref('images').list();
await Promise.all(
images.items.map(async item => {
const meta = await item.getMetadata();
if ((meta.contentType as string).startsWith('image')) {
result.push({
name: meta.name,
date: new Date(meta.updated).toLocaleString(),
url: urlPrefix + (meta.fullPath as string)
});
}
})
);
return result.sort((a: ImageType, b: ImageType) => {
const a_PathMilli = new Date(a.date);
const b_PathMilli = new Date(b.date);
return a_PathMilli < b_PathMilli ? 1 : -1;
});
} catch (error) {
console.log({ error });
}
};
その他の処理
ファイルのアップロードや削除は、ドキュメントを読めば特に詰まることなく実装できると思います。
動作確認
管理アプリからブログ記事の投稿、削除を行うと、VercelのDashboardで自動的にリビルド・デプロイされ、ブログサイトが更新されることを確認します。
また、アップロードした画像リンクをブログ記事に埋め込むと、画像が表示されることを確認します。
最後にFirebase Hostingにデプロイして終了です。
成果物
SignIn画面
管理画面
まとめ
ようやくJAMstackなブログサイトが完成しました。
Firebaseを使えば簡単にできるかなーって始めましたが、思ったより時間と労力がかかりました。調査7割、実装3割くらいの労力でした。
でも、調べて解決する分だけ、できることの幅が広がると思えば報われます。😌