Next.js 9を使ってCloud FunctionsとFirebase HostingでSSRする
皆さんこんにちは、noriです。
FirebaseSummitのレジストレーションがオープンしましたね。僕は今年も参加しますよ!!
今年はスペインの開催ですねー
楽しみ。
最近フロントの環境設定をする機会があったので、登場したばかりのNext.js 9をCloud Functionsにのせてみました。
Cloud Functionsにのせるところまでは、すぐだったんですがReactの扱いが初めてだった僕にとってつまりどころがあったので、同じことをしようとしている人たちのためにこの記事を書くことにしました。
先にサンプルコードを置いておきます。
Firebase HostingとしてNext.jsを動かす
まず今回やったことを図として示します。
ポイントは以下の2つです。
- Next.jsをCloud FunctionsにのせてSSR
- REST APIもCloud Functionsを利用する
Next.jsをCloud Functionsにのせる
実はこれはNext.jsのGitHubにサンプルが公開されているので全く難しくありません。
こちらのREADMEに従えば問題なく動作します。
※公開当日は動かなかったんですが、僕がPRを送ろうとした頃にすでに日本の方がコミットされてました。🤩
先こされた。。
https://github.com/zeit/next.js/commit/cdf4f0a20d71b365e69828942ca7bf69498fa825
手順を簡単に説明します。
Next.jsからテンプレートを取得
次のコマンドでサンプルコードを取得することができます。
このサンプルコードはそのままプロダクトに転用可能なので今回はこれを使いました。
npmの方はこちら
npx create-next-app --example with-firebase-hosting-and-typescript your-app
yarnの方はこちら
yarn create next-app --example with-firebase-hosting-and-typescript your-app
テンプレートを準備した時点でプロジェクトを実行可能になってます。次のコマンドで実行してみましょう。
npm run dev
このコマンドは、Next.jsを開発モードで起動するコマンドです。任意のブラウザが起動し、サンプルコードが起動するのが確認できればOKです。
Cloud Functionsにデプロイ
プロジェクトはすでに起動することが確認できたのでデプロイすれば動くはずですね。
早速Firebase側の設定をやってみましょう。
.firebaserc
を変更してFirebase プロジェクトの設定をしましょう。
この設定をせずにデプロイするともちろんエラーが出ます。
{
"projects": {
"default": "YOUR_PROJECT"
}
}
次のコマンドも忘れないように
firebase use default
これでFirebaseの設定も整いました。早速デプロイしてみましょう。
npm run deploy
Firebaseでは通常firebase deploy
でCloud Functionsへデプロイしますが、Next.jsを利用する場合はnpm run
を利用した方が良さそうです。
package.json
のscripts
の定義を見ると、それぞれのコマンドの前に色々と処理が入っていることがわかります。
例えば、serve
の実行前される、preserve
では次の処理が行われています。
- clean
- build-public
- build-functions
- build-app
- copy-deps
- install-deps
"scripts": {
"dev": "next src/app",
"preserve": "npm run clean && npm run build-public && npm run build-functions && npm run build-app && npm run copy-deps && npm run install-deps",
"serve": "cross-env NODE_ENV=production firebase serve",
"deploy": "npm run clean && firebase deploy",
"clean": "rimraf \"dist\"",
"build-app": "next build \"src/app\"",
"build-app-permission": "chmod 755 dist/functions/next/static/media/*.svg",
"build-public": "cpx \"src/public/**/*.*\" \"dist/public\" -C",
"build-functions": "tsc --project src/functions",
"lint-app": "tslint --project src/app",
"typecheck-app": "tsc --project src/app",
"lint-functions": "tslint --project src/functions",
"copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"dist/functions\" -C",
"install-deps": "cd \"dist/functions\" && npm i"
},
他のコマンドの実行に関してもスクリプトを経由した方が安全かと思います。
デプロイに関してはfirebase.json
に以下のように同じスクリプトが定義されているので実は安全に使えますが、念のためコマンドは入力方針は統一した方がいいでしょう。
"functions": {
"source": "dist/functions",
"predeploy": [
"npm run lint-functions",
"npm run lint-app",
"npm run typecheck-app",
"npm run build-functions",
"npm run build-app",
"npm run copy-deps",
"npm run install-deps",
"npm run build-app-permission"
]
},
デプロイしてFirebase HostingのURLを開ばNext.jsが動作していることが確認できるはずです。
Cloud FunctionsとFirebase Hosting
ここでCloud FunctionsとFirebase Hostingの関係性を説明しておきます。
Firebase Hostingは通常、静的コンテンツの配信を行う__Static Server__として機能します。
Firebase Hostingで動的コンテンツの配信を行う場合はfirebase.json
で設定の変更が必要です。
"hosting": {
"public": "dist/public",
"rewrites": [
{
"source": "**/**",
"function": "nextApp"
}
],
テンプレートを利用してる時点で設定は終わっているので今回はこのまま利用可能です。
また、Cloud Functionsでは次のようになっています。
import * as functions from 'firebase-functions'
import next from 'next'
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf: { distDir: 'next' } })
const handle = app.getRequestHandler()
export const nextApp = functions.https.onRequest((req, res) => {
console.log('File: ' + req.originalUrl)
return app.prepare().then(() => handle(req, res))
})
これからわかるように、https
を通したリクエストは全てNext.jsにコントロールが奪われていることがわかります。
Cloud FunctionsをREST APIにも対応させる
Cloud Functionsではexpress.jsを利用して__Path__をコントロールすることができます。
例えば次のように
const express = require('express');
const cors = require('cors');
const app = express();
// Automatically allow cross-origin requests
app.use(cors({ origin: true }));
// Add middleware to authenticate requests
app.use(myMiddleware);
// build multiple CRUD interfaces:
app.get('/:id', (req, res) => res.send(Widgets.getById(req.params.id)));
app.post('/', (req, res) => res.send(Widgets.create()));
app.put('/:id', (req, res) => res.send(Widgets.update(req.params.id, req.body)));
app.delete('/:id', (req, res) => res.send(Widgets.delete(req.params.id)));
app.get('/', (req, res) => res.send(Widgets.list()));
// Expose Express API as a single Cloud Function:
export const widgets = functions.https.onRequest(app);
__express__を使えばうまくいきそうですが、export
に注目すると
export const nextApp = functions.https.onRequest((req, res) => {
return app.prepare().then(() => handle(req, res))
})
export const widgets = functions.https.onRequest(app);
functions.https.onRequest
をいずれも占拠してしまっているため、共存できなさそうです。
今回は次のように設定しました。
index.ts
import API from 'api'
const dev = process.env.NODE_ENV !== 'production'
const nextApp = next({ dev, conf: { distDir: 'next' } })
const handle = nextApp.getRequestHandler()
export const hosting = functions.https.onRequest(async (req, res) => {
await nextApp.prepare()
const app = express()
app.use('/_', API)
app.get('*', async (req, res) => {
console.log('File: ' + req.originalUrl)
await handle(req, res)
})
app(req, res)
})
app.ts
const app = express()
app.post('/', (req, res, next) => {
res.status(200).send("REST API")
})
export default app
こうすることで次のことができるようになります。
-
/
で始まるパスのリクエストはNext.jsがコントロールする -
/_/
で始まるパスのリクエストはREST APIがコントロールする
これでNext.jsとREST APIをCloud Functionsに共存できるようになりました✨