「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ページを表示する
といったことが実現できます。
- 無料・簡単・片手でホームページを作成できる自社サービス Rakwi
- Web制作とアプリ開発を学べるオンラインプログラミング講座 Upstairs
- 開発,DX推進支援サービス スタートアッププラン