shuent.dev

📙React + TypeScript + Firebaseで認証付きの簡単な掲示板を作ろう

Sat Apr 10 2021

  • #react
  • #firebase

はじめに

つぶやきしかできないWebアプリを作りました。ReactでWebサービスを作りたい人はこのチュートリアルを読み進め、足りない機能を作ってみると良いでしょう。
Screen Shot 2021-04-11 at 16.43.53.png

Demo: https://single-board-3c001.web.app/
Code: https://github.com/shuent/single-board

Webアプリの機能

  • ログイン
    • Googleアカウントかメールアドレスで認証。
  • つぶやき一覧
  • コメント投稿
    • 認証したユーザーのみ投稿できる

使用する技術・ライブラリなど

  • React
    • Hooks
      • useReducer
    • ContextAPI
  • TypeScript
  • Create React App
  • React Router
  • Firebase
    • Auth
    • Firestore
    • Hosting
  • Chakra UI

Hooks

今どきなので、関数コンポーネントとHooksを使います。筆者は関数コンポーネントが出てからReactを勉強したので、Class時代のReactを書いたことがない。

TypeScript

  • コンパイル時エラーを出してくれる
  • IDEがコード補完しやすい

というメリットがあるので利用しています。初心者にとってコードを書く量が多くなるというデメリットを差し引いても、メリットが余りあります。

状態管理

状態管理にはHooksのuseReducerと Context APIを利用して、Fluxの思想を取り入れます。Reduxは使いませんが、実装の流れとしては一緒なので使い方は一度見ておくと良さそうです。

UIライブラリ: Chakra UI

Chakra UI は TailwindCSS のようなユーティリティベースなComponentを提供するUIライブラリです。Reactコンポーネントになっているので、Tailwindより使いやすく、Material UIなどのUIフレームワークよりは自由度があるので好きです。

Firebase

Firestore

今回はデータ構造が簡単なため、NoSQLであるFirestoreを利用します。NoSQLはDB設計に正解がないので難しく、複雑なリレーションを張るには向いてません。反面、バックエンドが要らず手軽に利用できるので、小規模で単純なデータ構造のアプリには使いやすいです。

Firebase Authentication

認証にはFirebase Authentication を利用します。tokenの管理などを裏でやってくれるので、とても楽です。さらにFirebase UIを使い、ログイン画面もほぼコード書かずに済みました。

Hosting

コマンド一つでデプロイ、urlを発行してくれます。今回はこれを使います

フロントエンドのホスティングサービスは他にもいろいろ出ています。Vercel, Netlify, Amplify. どれも簡単にデプロイできるので、試してみてください。

実装手順

  1. 画面設計・機能を書き出す
  2. データモデルの型を書き出す
  3. コンポーネント構造を考える
  4. 構造化して各フォルダ・ファイルを作る
  5. 画面を実装する。表示するのはダミーデータ
  6. ユーザー認証を実装
  7. Flux(useReducer + Context)でつぶやきの状態管理を実装
  8. firebaseから 読み取り、書き込みをできるようにする
  9. firestoreのルールを実装する
  10. デプロイする

次章から、ハンズオン形式でチュートリアルを書いていきます。コードを全て書いているわけではないので、説明が足りない部分はGithubリポジトリを参照してください。もしわからない部分があれば、質問していただけると記事の改善につながります。

画面設計・機能の書き出し

どんなアプリを作るにしても設計が大事です。まず一言で何を作るかを決めます。

  • 「ちょっと見た目に気を使ったシンプルなつぶやき投稿アプリ: Single Board」

機能一覧

次にアプリに欲しい機能を決めます。Single Boardは世に出す意識がなかったので、最低限と練習したい機能をつけることにしました。

  • 投稿一覧が見れる
  • 投稿にはユーザー情報と作成日時、つぶやき内容を載せる
  • ログインしないと投稿できない
  • googleとメールアドレスでログインできる

画面設計

紙でもUIツールでもパワポでも、ラフで良いので、画面を作ります。実際作ったのがこのくらいラフ。笑 トップページと、投稿コンポーネントを描いています。
IMG_2963.JPG
どうせUIライブラリに依存することになるので、丁寧にSketchやFigmaを使ってデザインする必要はないです。画面数が多い場合、雑にプロトタイプとしてFigmaかなんかで作ってみるのはアリ。

実装

ここからは実装していきます。まず、create-react-app(CRA)でプロジェクトを作成します。

1npx create-react-app single-board --template typescript

CRAでは必要なライブラリが全部入っているので、設定なしにコードを書き始められます。eslintも入っています。

ただ、コードフォーマットツールのprettierはありません。自動でコードを綺麗に整形したい人はインストールしましょう。vscodeを使っている人は、prettierの拡張機能をインストールすればnpm installする必要がありません。.prettierrcで自分の設定でコードフォーマットしてくれます。
参考:
https://create-react-app.dev/docs/setting-up-your-editor/#formatting-code-automatically
https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja

手元でできた画面を確認してみてください。デフォルトの画面が立ち上がります。

1npm start

プロジェクトフォルダの中の、基本的にはsrc/の中にコードを書いていくことになります。
それぞれの初期ファイルの説明は公式ページを読んでみてください。
https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja

データモデルの型を書き出す

プロジェクトを作った時、何から書いていけば良いか迷いますよね。データモデルから書くことによって、データ中心にアプリを作っていけるのでおすすめです。今回は、

  • つぶやき一覧でユーザー名とつぶやきのデータを表示する。
  • つぶやきを投稿する

という機能に必要なデータモデルを定義します。models.tsというファイルを作成します。

1.
2└── src/
3    └── models.ts
1export type IUser = {
2  displayName: string | null | undefined
3  photoURL: string | null | undefined
4}
5export type IComment = {
6  user: IUser
7  content: string
8  createdAt: Date
9  id: string
10}
11export type ICommentAdd = {
12  user: IUser
13  content: string
14}

表示に使う属性だけ定義します。

コンポーネント構造を考える

次にView、見た目の部分を作っていきます。Reactで開発する上で大事な考え方が、コンポーネント志向です。画面を適切な役割ごとにコンポーネントで切り分けて実装することで可読性、保守性が上がります。

コンポーネントの種類には2種類あります。

  • APIと通信したり、状態管理コードを呼んだり、状態を持っていたり、という副作用を持った実体コンポーネント
  • 受け取ったpropsを表示する純粋な関数コンポーネント
    • (Hooksを使ってもそのコンポーネント内で閉じているものも含む)

私が今回アプリを作っていくときには、

  • まず画面をざっくり前者の実体コンポーネント(と名付けてみる)で分けてみる。
  • 実体コンポーネントを実装する中で共通化できそうなものは関数コンポーネントに分けてみる

という風に作っていきました。

1- App
2- Home
3    - Header
4    - Editor
5    - CommentList
6        - Comment
7            - UserAvatar
8            - Content
9    - Footer
10- Login
11    - Header
12    - Form
13

考え方としては、Atomic Designを参考に、簡易化しています。実体コンポーネント、関数コンポーネントはそれぞれ Organism, molecules に対応するかと思います。

大事なのは、難しく考えずだいたいで切り分けてあとで共通化する、ということです。最初からDRYでやるのは悪手です。

実装していきます。

フォルダ・ファイルを構造化して作成する。

先に必要になりそうなファイルを全部作っていきます。

1.
2├── package-lock.json
3├── package.json
4├── public
5├── src
6│   ├── App.tsx # 各コンポーネントを呼び出す
7│   ├── api # firestoreのインターフェース
8│   │   └── commentsApi.ts
9│   ├── components
10│   │   ├── CommentList.tsx
11│   │   ├── Editor.tsx
12│   │   ├── Footer.tsx
13│   │   ├── Header.tsx
14│   │   ├── Home.tsx
15│   │   ├── Login.tsx
16│   │   ├── MainVisual.tsx
17│   │   └── UserAvatar.tsx
18│   ├── contexts 
19│   │   ├── authContext.tsx # ユーザー認証状態管理
20│   │   └── commentsContext.tsx # つぶやきの状態管理
21│   ├── reducers 
22│   │   └── commentsReducer.ts # つぶやきのFlux (あとで解説)
23│   ├── firebase.ts
24│   ├── index.tsx # App.tsxを呼び出しているだけ
25│   ├── models.ts # データモデル
26│   └── theme.ts # 全体UIの設定
27└── tsconfig.json

Viewを作る

UIライブラリのChakra UIをインストールします。

1npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4

react-routerを使い、urlによって、トップ画面とログイン画面を出し分けます。公式ドキュメントではサンプルを動かせるので、めちゃわかりやすいです。

1npm i react-router-dom
1import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
2
3import { Header } from './components/Header'
4import { Login } from './components/Login'
5import { Home } from './components/Home'
6
7function App() {
8  return (
9    <Router>
10      <Header />
11      <Switch>
12        <Route exact path='/'>
13          <Home />
14        </Route>
15        <Route path='/login'>
16          <Login />
17        </Route>
18      </Switch>
19    </Router>
20  )
21}
22
23export default App
1import { CommentList } from './CommentList'
2import { MainVisual } from './MainVisual'
3import { Editor } from './Editor'
4import { Footer } from './Footer'
5export const Home = () => (
6  <>
7    <MainVisual /> // 一番上のメインビジュアル
8    <Editor />  // つぶやき編集フォーム
9    <CommentList /> // つぶやきリスト
10    <Footer /> // フッター
11  </>
12)

ダミーデータを作り、とりあえず表示するの画面を作っていきます。

1import { HStack, Box, Avatar, Heading, Text } from '@chakra-ui/react'
2import { IComment, IUser } from '../models'
3
4// ダミーデータ
5const user1: IUser = { displayName: 'testuser1', photoURL: 'sample.jpg' }
6const dcomments: IComment[] = [
7    {
8      user: user1,
9      content:
10        'first comment ss',
11      createdAt: new Date(),
12      id: 'comment1id',
13    },
14    {
15      user: user1,
16      content: '元気ですか',
17      createdAt: new Date(),
18      id: 'comment2id',
19    },
20  
21export const CommentList = () => {
22  return (
23    <>
24      <Heading>
25        Posted Comments
26      </Heading>
27      <ul>
28        {comments === [] ? (
29          <p>No Post</p>
30        ) : (
31          // Comment 実装は省略
32          comments.map((comment) => (
33            <Comment key={comment.id} comment={comment} />  
34          ))
35        )}
36      </ul>
37    </>
38  )
39}

省略したコンポーネントはレポジトリをみてみてください。

認証画面・機能を作成

ログイン画面を作っていきます。Firebase AuthenticationとFirebaseUIを使うことで簡単に実装できます。

firebaseの設定

firebase consoleでプロジェクトを作成します。
https://console.firebase.google.com/u/0/?hl=ja

作成したら、プロジェクトの設定 > Firebase SDK snippet を取得します。
Screen Shot 2021-04-12 at 20.51.40.png

CRAは元々の設定で、REACT_APP_から始まる環境変数名を.envファイルからアプリに組み込んでくれます。そして、ビルド時に値を埋め込んでくれます。これで外に変数が漏れることはありません。
https://create-react-app.dev/docs/adding-custom-environment-variables/

先ほど取得した値を変数として.local.envファイルに宣言し、プロジェクトのルートにおきます。

1  REACT_APP_APIKEY=xxxxxx
2  REACT_APP_AUTHDOMAIN=xxxxxx
3  REACT_APP_PROJECTID=xxxxxx
4  REACT_APP_STORAGEBUCKET=xxxxxx
5  REACT_APP_MESSAGINGSENDERID=xxxxxx
6  REACT_APP_APPID=xxxxxx
7  REACT_APP_MEASUREMENTID=xxxxxx

プロジェクト内では、firebaseを扱うファイルを作り、環境変数を埋めます。ついでにFirebaseの認証とデータベースにfirestoreを使うので、exportしておきます。

1import firebase from 'firebase'
2
3const fireConfig = {
4  apiKey: process.env.REACT_APP_APIKEY,
5  authDomain: process.env.REACT_APP_AUTHDOMAIN,
6  projectId: process.env.REACT_APP_PROJECTID,
7  storageBucket: process.env.REACT_APP_STORAGEBUCKET,
8  messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID,
9  appId: process.env.REACT_APP_APPID,
10  measurementId: process.env.REACT_APP_MEASUREMENTID,
11}
12firebase.initializeApp(fireConfig)
13const auth = firebase.auth()
14const firedb = firebase.firestore()
15export { firebase, auth, firedb }

firebaseUIを導入

ログイン画面を作っていきます。
firebaseUIのReact用ライブラリがあるのでインストールします。

*公式の開発者がストップしているみたいなので、canary版を使います。
https://github.com/firebase/firebaseui-web-react/pull/122

1npm install react-firebaseui@canary

ログインコンポーネントを作ります。ログインフォームの挙動はuiConfig変数で設定します。

1import { Center, Heading, VStack } from '@chakra-ui/layout'
2import { primaryTextColor } from '../theme'
3import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
4import { firebase, auth } from '../firebase'
5
6const uiConfig = {
7  signInFlow: 'popup',
8  signInSuccessUrl: '/',
9  signInOptions: [
10    firebase.auth.GoogleAuthProvider.PROVIDER_ID,
11    firebase.auth.EmailAuthProvider.PROVIDER_ID,
12  ],
13}
14
15export const Login = () => {
16  return (
17    <Center mt={8}>
18      <VStack>
19        <Heading size='md' color={primaryTextColor}>
20          Sign In
21        </Heading>
22        <StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth} />
23      </VStack>
24    </Center>
25  )

<StyledFirebaseAuth firebaseAuth={auth} />でプロジェクトのfirebaseインスタンスとUIをつなげています。

AuthContextで認証状態管理

ログイン・登録ができるようになったので、セッション情報:(「ログインしているかどうか」と「ログインしているユーザー情報」)をアプリ内で使えるようにします

ユーザー認証の状態管理には、Context APIを使用します。流れとしては、Contextを作成し、Providerで状態を保存し、useContextで使います。
公式: https://ja.reactjs.org/docs/context.html

認証用のContextを扱う、authContext.tsを作成します。

1import React, { createContext, useContext, useState, useEffect } from 'react'
2import { firebase, auth } from '../firebase'
3
4type AuthContextProps = {
5  user: firebase.User | null
6}
7
8const AuthContext = createContext<AuthContextProps>({
9  user: null,
10})
11
12export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
13  const [user, setUser] = useState<firebase.User | any>(null)
14  const [loading, setLoading] = useState(true)
15
16  useEffect(() => {
17    const unsubscribe = auth.onAuthStateChanged((user) => {
18      setUser(user)
19      setLoading(false)
20    })
21    return unsubscribe
22  }, [])
23
24  return (
25    <AuthContext.Provider value={{ user }}>
26      {!loading && children}
27    </AuthContext.Provider>
28  )
29}
30
31export const useAuth = () => {
32  return useContext(AuthContext)
33}
34

auth.onAuthStateChangedではユーザーの認証状態を監視して、ログイン、ログアウト時、と認証情報が変わる度に引数に渡しているコールバック関数を実行します。
ProviderをUnmountする時に監視を捨てる必要があるので、useEffectの返り値に設定してます。
https://firebase.google.com/docs/auth/web/manage-users?hl=ja

セッションを使用する

今定義した関数を使い、アプリ上でセッションを取得できるようにしましょう。アプリ全体をAuthProviderで囲みます。これで囲んだどのコンポーネント内でもuseAuth()が使えることになります。

1
2...
3import { AuthProvider } from './contexts/authContext'
4
5function App() {
6  return (
7+    <AuthProvider>
8       <ChakraProvider theme={theme}>
9          <Router>
10            <Header />
11            <Switch>
12              <Route exact path='/'>
13                <Home />
14              </Route>
15              <Route path='/login'>
16                <Login />
17              </Route>
18            </Switch>
19          </Router>
20       </ChakraProvider>
21+    </AuthProvider>
22  )
23}

ヘッダーでログインしている時はログアウトボタン、ログインしていないときはログインリンクを表示します。

1import { Link } from 'react-router-dom'
2import { useAuth } from '../contexts/authContext'
3import { auth } from '../firebase'
4
5export const Header = () => {
6  const { user } = useAuth()
7  return (
8  <>
9    // ...省略
10    {user ? (
11      <Text as='button' onClick={() => auth.signOut()}>
12        Log Out
13      </Text>
14    ) : (
15      <Link to='/login'>
16        <Text color='white'> Sign In</Text>
17      </Link>
18    )}
19    // ...
20  </>
21  )
22

トップ画面のエディターでもログインしている時のみ投稿できるようにします。

1export const Editor = () => {
2  const { user } = useAuth()
3  const [content, setContent] = useState('')
4
5  const handleSubmit = (e: React.FormEvent) => {
6    e.preventDefault()
7    if (content !== '' && user) {
8      // post content to server
9    } else if (!user) {
10      alert('Sign in first')
11    }
12    setContent('')
13  }
14  const handleChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
15    setContent(e.currentTarget.value)
16  }
17
18  return (
19    <div>
20      <VStack
21        as='form'
22        onSubmit={handleSubmit}
23      >
24        <Textarea
25          name='content'
26          value={content}
27          onChange={handleChange}
28          placeholder="What's on your mind?"
29        />
30        <Button type='submit' colorScheme='orange'>
31          post
32        </Button>
33      </VStack>
34    </div>
35  )
36}

これで認証状態管理は終わりです。

つぶやきの状態管理

つぶやきの状態管理でもContextAPIを使い、状態を保持できるようにします。加えて、useReducerというHookを使いFluxアーキテクチャでの状態管理を行います。Contextだけでも管理できないことはないですが、状態を変更する機能が多くなってきた時に分かりやすいです。

reducerから定義していきます。

1import { IComment } from '../models'
2
3export type CommentsAction =
4  | { type: 'SET_COMMENTS'; comments: IComment[] }
5  | { type: 'ADD_COMMENT'; comment: IComment }
6
7export type CommentsState = {
8  comments: IComment[]
9}
10
11export const initialState: CommentsState = {
12  comments: [],
13}
14
15export const commentsReducer = (
16  state: CommentsState,
17  action: CommentsAction
18): CommentsState => {
19  switch (action.type) {
20    case 'SET_COMMENTS':
21      return { comments: action.comments }
22    case 'ADD_COMMENT':
23      return { comments: [action.comment, ...state.comments] }
24    default:
25      return state
26  }
27

後々firestoreにつぶやきを投稿したタイミングでまたつぶやきリストを取得するかストリーミングすれば最新の状態になるので、ADD_COMMENTはあってもなくても良いのですが、毎回APIを呼ばなくても良いようにと、練習のために作っています。

Contextを作ります。

1import {
2  createContext,
3  Dispatch,
4  ReactNode,
5  useReducer,
6  useContext,
7} from 'react'
8import {
9  CommentsAction,
10  commentsReducer,
11  CommentsState,
12  initialState,
13} from '../reducers/commentsReducer'
14
15type CommentsContextProps = {
16  state: CommentsState
17  dispatch: Dispatch<CommentsAction>
18}
19
20const CommentsContext = createContext<CommentsContextProps>({
21  state: initialState,
22  dispatch: () => initialState,
23})
24
25export const CommentsProvider = ({ children }: { children: ReactNode }) => {
26  const [state, dispatch] = useReducer(commentsReducer, initialState)
27  return (
28    <CommentsContext.Provider value={{ state, dispatch }}>
29      {children}
30    </CommentsContext.Provider>
31  )
32}
33
34export const useComments = () => useContext(CommentsContext)
35

EditorとCommentListで使うので、それらを含むHomeコンポーネントで囲んでおきます。

1export const Home = () => (
2<>
3+  <CommentsProvider>
4     <MainVisual />
5     <Editor />
6     <CommentList />
7     <Footer />
8+  </CommentsProvider>
9<>
10)

フォーム送信時にDispatchします。

1export const Editor = () => {
2  const { user } = useAuth()
3  const { dispatch } = useComments()
4  const [content, setContent] = useState('')
5
6  const handleSubmit = (e: React.FormEvent) => {
7    e.preventDefault()
8    if (content !== '' && user) {
9      const toPost: ICommentAdd = {
10        user: { displayName: user.displayName, photoURL: user.photoURL },
11        content,
12      }
13      dispatch({
14        type: 'ADD_COMMENT',
15        comment: {
16          ...toPost,
17          createdAt: new Date(),
18          id: Date(),
19        },
20      })
21    } else if (!user) {
22      alert('Sign in first')
23    }
24    setContent('')
25  }
26
27return (...)
28}

useEffectでコンポーネントを読み込むタイミングでDispatchします。

1export const CommentList = () => {
2  const { state, dispatch } = useComments()
3  const dcomments: IComment[] = [
4    {
5      user: user1,
6      content:
7        'first comment',
8      createdAt: new Date(),
9      id: 'comment1id',
10    },
11    {
12      user: user1,
13      content: '元気ですか',
14      createdAt: new Date(),
15      id: 'comment2id',
16    },
17  ]
18
19  useEffect(() => {
20    let unmount = false
21    if (!unmount) {
22      console.log('set comments called')
23      dispatch({ type: 'SET_COMMENTS', comments: dcomments })
24    }
25    return () => {
26      unmount = true
27    }
28  }, [dispatch])
29
30  return (...)
31}

これでつぶやき(コメント)の状態管理は終わりです。

firestore への read/write

firestore上でデータを管理できるようにします。

firebaseコンソールで firestoreを有効にします。

firestoreへのインターフェースを実装します。ここで実装することで、将来別のAPIを使った時にも 関数名、引数、返り値を同じにすることでView側を変更しなくても良いように、疎結合に実装します。もっと厳密にやるならinterfaceを定義したり、Dipendency Injectionをすることになります。

1import { firedb, firebase } from '../firebase'
2import { IComment, ICommentAdd } from '../models'
3
4export const getComments = async () => {
5  const snapShot = await firedb
6    .collection('comments')
7    .orderBy('createdAt', 'desc')
8    .get()
9  const data = snapShot.docs.map<IComment>((doc) => ({
10    user: doc.data().user,
11    content: doc.data().content,
12    createdAt: doc.data().createdAt.toDate(),
13    id: doc.id,
14  }))
15  return data
16}
17
18export const addComment = async (comment: ICommentAdd) => {
19  return firedb.collection('comments').add({
20    user: comment.user,
21    content: comment.content,
22    createdAt: firebase.firestore.Timestamp.now(),
23  })
24

使用時には、主にDispatch呼び出し前におき、結果をDispatchに渡します。

コメントリスト

1export const CommentList = () => {
2  const {
3    state: { comments },
4    dispatch,
5  } = useComments()
6  useEffect(() => {
7+    getComments().then((data) => {
8+      dispatch({ type: 'SET_COMMENTS', comments: data })
9+    })
10  }, [dispatch])
11
12  return (...)
13}

エディター

1export const Editor = () => {
2  const { user } = useAuth()
3  const { dispatch } = useComments()
4  const [content, setContent] = useState('')
5
6  const handleSubmit = (e: React.FormEvent) => {
7    e.preventDefault()
8    if (content !== '' && user) {
9      const toPost: ICommentAdd = {
10        user: { displayName: user.displayName, photoURL: user.photoURL },
11        content,
12      }
13+      addComment({ ...toPost })
14      dispatch({
15        type: 'ADD_COMMENT',
16        comment: {
17          ...toPost,
18          createdAt: new Date(),
19          id: Date(),
20        },
21      })
22    } else if (!user) {
23      alert('Sign in first')
24    }
25    setContent('')
26  }

ブラウザでつぶやいてみると、firestoreにもデータが追加されているのが分かります
Screen Shot 2021-04-12 at 22.43.22.png

firestore rule

Editor Componentで、ユーザーではない場合投稿できないようにしましたが、直接APIを知られてしまった場合、投稿できてしまいます。さらに今のままだと投稿するユーザー名を偽装して、本人以外の名を騙り投稿できてしまいます。

そのようなことがないように、コンソールでruleを書くことで、セキュリティを守ります。ローカル環境で書いてデプロイすることも可能ですが、ここではコンソールに直接書いてます。

Screen Shot 2021-04-12 at 22.53.16.png

左下のルールプレイグラウンドでは、いろいろな条件でルールをテストできるので、試してみると良いです。

今回のルールはこちら

  • 'comments'以外のリソースにアクセスできない
  • (ログインしてなくても)誰でも読めるようにする
  • ユーザー名と一致する投稿のみ受け付ける。
  • 更新、削除は受け付けない
1rules_version = '2';
2service cloud.firestore {
3  match /databases/{database}/documents {
4    match /{document=**} {
5      match /comments/{comment} {
6        allow read: if true;
7        allow create: if request.auth.token.name == request.resource.data.user.displayName
8      }
9    }
10  }
11}

公式: https://firebase.google.com/docs/rules/basics?hl=ja

Firebase Hosting へデプロイ

ここまででアプリが完成したらFirebase Hostingサービスにデプロイします。

コンソールからHostingを有効にします。

1# firebase cliをインストールして、deployコマンドを使えるようにします。
2npm install -g firebase-tools
3# 認証してコンソールで作ったプロジェクトを選択します。
4firebase login

firebaseのファイルを作成します。

1firebase init

いろいろ聞かれます。Hostingだけ選択し、
What do you want to use as your public directory? には buildを指定します。
あとは好きなものを選んでください。

最終的にこんなファイルができていれば大丈夫です。

1{
2  "hosting": {
3    "public": "build",
4    "ignore": [
5      "firebase.json",
6      "**/.*",
7      "**/node_modules/**"
8    ],
9    "rewrites": [
10      {
11        "source": "**",
12        "destination": "/index.html"
13      }
14    ]
15  }
16}

Githubと連携してCICDやプルリクでデプロイしてくれるのですが、その設定は扱いません。調べてみてください。

プロジェクトをビルド後、デプロイします!

1npm run build
2firebase deploy --only hosting

うまくいけば、ターミナルに出てくるurlがデプロイ先です!!!

終わりに

今回作ったアプリにはつぶやきの削除、ユーザー設定、ユーザーページなど機能が足りません。ここまで読んでくれた方はこれを発展させて、改造させて、面白いものを作ってみてください。もし作った時はコメントから報告してくれると嬉しいです。

初めて包括的な記事を書いたので足りないところはあると思いますが、楽しんでいただけたら何らかのアクションをしてくれると嬉しいです。ここまで読んでくれてありがとうございました。次回はもうちょっと高度なことか、コンポーネント設計に関することを書きたいと思います。