Edited at

FirebaseとReact+TypeScriptの連携


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を行う必要があります


/functions/src/index.ts

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を再起動する必要があります


/firebase.json

{

"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との通信機能の実装


/front/src/MessageModule.ts

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[] });
});
}
}


・入力フォーム


/front/src/MessageForm.tsx

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


・メッセージ表示


/front/src/MessageList.tsx

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


・トップモジュール


/front/src/index.tsx

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/

以下のように表示されます

image.png


3.デプロイ

以下のコマンドを使ってしばらく待ちます

npm deploy

Hosting URLとして表示されたアドレスで、掲示板が表示されます

初回は書き込みに15秒くらいかかりますが、その後はすぐに応答するようになります


4.まとめ

 Firebaseへのアクセスはfunctionsを経由すると、フロントエンド側でキーの管理をする必要がなくなるので、とてもやりとりが楽になります。さらに全てをいったんhosting経由にすることによって、ドメインが分散しなくなり、開発環境と本番環境でAjaxでのアクセス先を調整する必要も無くなりました。こんなに色々出来るのに、これらが全部無料で使えるとは素晴らしい時代になったと思いました。