JavaScript

Next.jsのSSG(SSR)をCloud FunctionsとFirebase Hostingで動かした話

「Next.jsのgetStaticPropsがFirebase Hostingでは動かない…」
「サーバーサイドの処理なので、Cloud Functionsを組み合わせる必要がありそう」
「Cloud FunctionsでSSG or SSRしたページを表示させたい」

という悩みを抱える方向けの記事です。

この記事を執筆した2020年9月16日時点では、Next.jsとCloud Functions&Firebaes Hostingの相性があまり良くないのではと疑っています

Firebase AuthenticationとFirestoreは必ず使いたいものの、ホスティングはVercelにしたほうがいいかな?と検討中。

上記を踏まえた上で、以下の内容をご覧ください。

 

ディレクトリ構成を確認しよう

ディレクトリ構成は以下の通りです。srcディレクトリ以下にすべてを入れます。

src
 ├── app
 │   ├── components
 │   ├── firebase
 │   ├── lib
 │   ├── next-env.d.ts
 │   ├── next.config.js
 │   ├── pages
 │   ├── reducks
 │   ├── styles
 │   ├── tsconfig.json
 │   ├── tslint.json
 │   └── types
 ├── functions
 │   ├── firestore.js
 │   └── index.js
 └── public

ポイントはappディレクトリとfunctionsディレクトリを用意することです。

各ディレクトリの役割
  • appディレクトリ:Next.jsやReact関連のファイルを格納する
  • functionsディレクトリ:Cloud Functions関連のファイルを格納する
  • publicディレクトリ:画像などの静的ファイルを格納する

気になるファイルの内容は後ほど紹介します。

リクエストをCloud Functionsで受け付けるよう設定変更しよう

通常、FirebaseでSPAをホスティングする際には、publicディレクトリにindex.htmlを用意して、すべてのリクエストをindex.htmlを参照するようにします。

ですが、Next.jsで開発したアプリをSSGやSSRを使いながらホスティングするには、サーバーサイドでコードを動かす必要があります。

なので今回はCloud Functionsですべてのリクエストを受け付けて、Webページを表示したいと思います。

なお、Cloud Functionsで通常のAPIも使いたいので、Next.jsとは別に、expressを用いてAPI用のリクエストを受け付けるURLも作成します。

package.jsonを変更する

一部関係ないパッケージも含まれてますが、丸っと載せます。
scriptsやmainがポイントです。

{
  "name": "hoge",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": "10"
  },
  "main": "src/functions/index.js", // mainにCloud Functionsのindex.jsを指定する
  "scripts": {
    "dev": "next src/app",
    "preserve": "npm run build-public && npm run build-app",
    "serve": "firebase serve",
    "clean": "rimraf \".next\"",
    "clean-app": "find -E src/app/*/ -type f -iregex \".*\\.(js|jsx)\" | xargs rm -rf",
    "build-app": "next build \"src/app\"",
    "build-public": "cpx \"src/public/**/*.*\" \"public\" -C",
    "lint-app": "tslint --project src/app",
    "typecheck-app": "tsc --project src/app",
  },
  "dependencies": {
    "@material-ui/core": "^4.11.0",
    "@material-ui/icons": "^4.9.1",
    "@material-ui/lab": "^4.0.0-alpha.56",
    "@material-ui/styles": "^4.10.0",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "clsx": "^1.1.1",
    "cors": "^2.8.5",
    "csv-parse": "^4.12.0",
    "express": "^4.17.1",
    "firebase": "^7.18.0",
    "firebase-admin": "^9.1.1",
    "firebase-functions": "^3.11.0",
    "html-react-parser": "^0.13.0",
    "next": "9.5.1",
    "next-routes": "^1.4.2",
    "node-fetch": "^2.6.0",
    "react": "16.13.1",
    "react-copy-to-clipboard": "^5.0.2",
    "react-dom": "16.13.1",
    "react-redux": "^7.2.1",
    "react-scripts": "3.4.3",
    "redux": "^4.0.5",
    "redux-actions": "^2.6.5",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0",
    "reselect": "^4.0.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.7",
    "@types/csv-parse": "^1.2.2",
    "@types/firebase": "^3.2.1",
    "@types/node": "^14.0.27",
    "@types/node-fetch": "^2.5.7",
    "@types/react": "^16.9.44",
    "@types/react-copy-to-clipboard": "^4.3.0",
    "@types/react-redux": "^7.1.9",
    "@types/redux-logger": "^3.0.8",
    "@types/redux-thunk": "^2.1.0",
    "@types/webpack": "^4.41.22",
    "child_process": "^1.0.2",
    "cpx": "^1.5.0",
    "cross-env": "^7.0.2",
    "firebase-functions-test": "^0.2.2",
    "firebase-tools": "^8.10.0",
    "husky": "^4.2.5",
    "lint-staged": "^10.2.11",
    "prettier": "^2.0.5",
    "rimraf": "^3.0.2",
    "ts-loader": "^8.0.3",
    "ts-node": "^8.10.2",
    "tslint": "^6.1.3",
    "tslint-react": "^5.0.0",
    "typescript": "^3.9.7"
  }
}

 

firebase.jsonを変更する

次にfirebase.jsonを書き換えます。
ポイントは、predeployに指定するnpm scriptsと、hostingのrewritesルールです。

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": {
    "source": ".",
    "predeploy": [
      "npm run typecheck-app",
      "npm run build-app"
    ],
    "runtime": "nodejs10"
  },
  "hosting": {
    "public": "public",
    "predeploy": "npm run build-public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/v1/**/*",    // APIを受け付けるURL
        "function": "api"    // Cloud Functionsのapiという関数を実行する
      },
      {
        "source": "**/**",    // API以外のURLをすべて受け付けるURL
        "function": "nextApp"    // Cloud FunctionsのnextAppという関数を実行する
      }
    ]
  },
  "storage": {
    "rules": "storage.rules"
  }
}

 

Cloud Functions用のindex.jsを書こう

const admin = require('firebase-admin');
admin.initializeApp()
const functions = require('firebase-functions');
const { default: next } = require('next');
const cors = require('cors');
const express = require('express')
const { fetchUsers } = require('./firestore')

// Declare HTTP Request Function for Next.js App
const app = next({ dev: false, conf: { distDir: '.next'} })
const handle = app.getRequestHandler()
exports.nextApp = functions.https.onRequest((req, res) => {
  console.log('File: ' + req.originalUrl)
  return app.prepare().then(() => handle(req, res))
})

// APIのレスポンス用関数
const sendResponse = (response, statusCode, body) => {
  response.send({
    statusCode,
    headers: { 'Access-Control-Allow-Origin': '*' },
    body: JSON.stringify(body),
  })
}

// Declare HTTP Function for API request
const server = express();
server.use(cors({ origin: true }));
// getリクエストを作成。fetchUsers関数の実行結果をレスポンスのbodyとして返す
server.get("/v1/users", async (req, res) => sendResponse(res, 200, await fetchUsers() ));

exports.api = functions.https.onRequest(server)

 

  • /v1から始まるURLへのリクエストには、api関数を実行してレスポンスを返す
  • それ以外のURLへのリクエストはNext.jsのWebページを表示する

といったことが実現できます。

ABOUT ME
稲垣 貴映
サーバーエンジニア兼Webフロントエンジニア。 新卒で独立系SIerに入社後、金融系システム基盤の構築・運用を3年間経験。 プログラミングを独学していた頃に、CEOの馬谷が開講していたSwiftスクールに通い、2019年9月に入社。 趣味はブラジル音楽の演奏。

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA