FirebaseとReact+TypeScriptの連携
※同じ記事をこちらにも書いています
今回は以下の機能を利用します
- Firebase
- hosting 静的コンテンツの設置とfunctionsへのプロキシ
- functions databaseへの入出力
- database データ保存
Webからのアクセスはfunctionsも含め、必ずhostingを介するようにします。これで静的コンテンツとデータアクセス用の窓口のドメインが同一になるので、CORS対策で余計な処理を付け加える必要がなくなります。
Reactに関してはトランスコンパイル後のファイルを静的コンテンツとしてhostingに配置します。フロントエンドからFirebaseのdatabaseへのアクセスはfunctions経由となるので、フロントエンド側にAPIキーの設定をする必要はありません。今回はキーを一切使わないコードとなっています。
1.基本設定
1.1 Firebaseツールのインストール
Node.jsが入っていることが前提です
以下のコマンドで必要なツールをインストールします
npm -g i firebase-tools
1.2 Firebase上にプロジェクトを作成
2019/10/18 17:16:45
https://console.firebase.google.com/へ行って、プロジェクトを作成します。
コマンドラインから作ることも可能ですが、IDが被った場合に対処しにくいので、Web上から作った方が簡単です。
ローカルに開発環境を作る
まずは新規ディレクトリを作って、そこをカレントディレクトリにしてください
・Firebaseにログインし、コマンドからの操作をするための権限を取得する
firebase login
・質問が出たらエンターキー
? Allow Firebase to collect CLI usage and error reporting information?
・ブラウザからユーザ認証
・Firebaseプロジェクトの作成
firebase init
? Are you ready to proceed? (Y/n) <- エンターキー
・Database、Functions、Hostingを選ぶ
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
(*) Database: Deploy Firebase Realtime Database Rules
( ) Firestore: Deploy rules and create indexes for Firestore
(*) Functions: Configure and deploy Cloud Functions
(*) Hosting: Configure and deploy Firebase Hosting sites
( ) Storage: Deploy Cloud Storage security rules
・プロジェクトの選択は先ほど作ったプロジェクトを選ぶ
? Please select an option: (Use arrow keys)
> Use an existing project <- これを選択
・データベースのルール
? What file should be used for Database Rules? database.rules.json <- エンターキー
・開発言語の選択
TypeScript
・TSLintはNo
・残りは全てエンターキー
1.3 React環境の構築
package.jsonの作成
npm -y init
React環境構築パッケージのインストール
npm -D i setup-template-firebase-react
必要パッケージ類のインストールを確定させる
npm i
Reactのビルド(自動ビルドの場合はnpm run watch)
npm run build
インストールしたパッケージは以下のような構造を作ります
root/
├ public/ (ファイル出力先)
└ front/ (フロントエンド用ディレクトリ)
├ public/ (リソースHTMLファイル用)
│ └ index.html
├ src/ (JavaScript/TypeScript用ディレクトリ)
│ ├ .eslintrc.json
│ ├ index.tsx
│ └ tsconfig.json
└ webpack.config.js
1.4 firebaseエミュレータの起動
エミュレータの起動
(functionsがコンパイルされてないのでエラーを出しますが、ここでは無視してください)
npm start
確認
http://localhost:5000/
「今日は世界!」と表示されればOK
2.掲示板を作る
2.1 functionsの設定
データが送られてきたらdatabaseに書き込み、そうでない場合もdatabase上のデータを返すプログラムです
作成が終わったらfunctionsディレクトリでnpm buildを行う必要があります
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp(functions.config().firebase);
export const proc = functions.https.onRequest((request, response) => {
(async () => {
try {
const { name, body } = request.body;
if (name && body) {
const date = new Date().toISOString();
await admin
.database()
.ref("/bbs")
.push({ name, body, created_at: date, update_at: date });
}
} catch {}
})();
admin
.database()
.ref("/bbs")
.orderByChild("created_at")
.on("value", data => {
const values = data!.val();
const result = Object.entries(values).map(([key, value]) => ({
...value,
id: key
}));
response.status(200).send(result);
});
});
2.2 hostingの設定
functionsで作成したコードがhostingを経由できるように、rewritesの設定を追加します
これをやったら、firebaseのemulaterを再起動する必要があります
{
"database": {
"rules": "database.rules.json"
},
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
],
"source": "functions"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "/proc",
"function": "proc"
},
{
"source": "**",
"destination": "/index.html"
}
]
}
}
2.3 フロントエンド側の作成
ここから先はReactのフロントエンドプログラムとなります
2.3.1 パッケージの追加インストール
フロントエンド側で状態管理や通信を行うためのモジュールを追加します
npm -D i react-redux @types/react-redux @jswf/redux-module @jswf/adapter
2.3.2 ソースコードの追加
・Storeデータ管理とFirebaseとの通信機能の実装
import { ReduxModule } from "@jswf/redux-module";
import { Adapter } from "@jswf/adapter";
//メッセージの構造
interface Message {
id: number;
name: string;
body: string;
created_at: Date;
updated_at: Date;
}
//Storeで使う構造
interface State {
messages: Message[];
}
//データモジュールの定義
export class MessageModule extends ReduxModule<State> {
protected static defaultState: State = {
messages: []
};
public write(message: { name: string; body: string }) {
Adapter.sendJsonAsync("proc", message).then(e => {
if (e instanceof Array) this.setState({ messages: e as Message[] });
});
}
public load() {
Adapter.sendJsonAsync("proc").then(e => {
if (e instanceof Array) this.setState({ messages: e as Message[] });
});
}
}
・入力フォーム
import { useRef } from "react";
import React from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";
export function MessageForm() {
const messageModule = useModule(MessageModule, undefined, true);
const message = useRef({ name: "", body: "" }).current;
return (
<div>
<div>
<button
onClick={() => {
messageModule.write(message);
}}
>
送信
</button>
</div>
<div>
<label>
名前
<br />
<input onChange={e => (message.name = e.target.value)} />
</label>
</div>
<div>
<label>
メッセージ
<br />
<input onChange={e => (message.body = e.target.value)} />
</label>
</div>
</div>
);
}
・メッセージ表示
import React, { useEffect } from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";
export function MessageList() {
const messageModule = useModule(MessageModule);
//初回のみメッセージを読み込む
useEffect(() => {
messageModule.load();
}, []);
//メッセージをStoreから取得
const messages = messageModule.getState("messages")!;
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<hr />
<div>
[{msg.name}]{msg.created_at} -- ({msg.id})
</div>
<div>{msg.body}</div>
</div>
))}
</div>
);
}
・トップモジュール
import React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { ModuleReducer } from "@jswf/redux-module";
import { MessageForm } from "./MessageForm";
import { MessageList } from "./MessageList";
function App() {
return (
<>
<MessageForm />
<MessageList />
</>
);
}
const store = createStore(ModuleReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root") as HTMLElement
);
2.3.3 確認
データはemulaterを再起動すると消えます
emulaterの起動
npm start
2.3.4 確認
http://localhost:5000/
以下のように表示されます
3.デプロイ
以下のコマンドを使ってしばらく待ちます
npm deploy
Hosting URLとして表示されたアドレスで、掲示板が表示されます
初回は書き込みに15秒くらいかかりますが、その後はすぐに応答するようになります
4.まとめ
Firebaseへのアクセスはfunctionsを経由すると、フロントエンド側でキーの管理をする必要がなくなるので、とてもやりとりが楽になります。さらに全てをいったんhosting経由にすることによって、ドメインが分散しなくなり、開発環境と本番環境でAjaxでのアクセス先を調整する必要も無くなりました。こんなに色々出来るのに、これらが全部無料で使えるとは素晴らしい時代になったと思いました。