Help us understand the problem. What is going on with this article?

Next.jsを使ってCloud FunctionsとFirebase HostingでSSRする

Next.js 9を使ってCloud FunctionsとFirebase HostingでSSRする

皆さんこんにちは、noriです。
FirebaseSummitのレジストレーションがオープンしましたね。僕は今年も参加しますよ!!
今年はスペインの開催ですねー
楽しみ。

最近フロントの環境設定をする機会があったので、登場したばかりのNext.js 9をCloud Functionsにのせてみました。

Cloud Functionsにのせるところまでは、すぐだったんですがReactの扱いが初めてだった僕にとってつまりどころがあったので、同じことをしようとしている人たちのためにこの記事を書くことにしました。

先にサンプルコードを置いておきます。

https://github.com/1amageek/ballcap.ts/tree/master/examples

Firebase HostingとしてNext.jsを動かす

まず今回やったことを図として示します。

Next.js on Firebase Hosting, Cloud Functions

ポイントは以下の2つです。

  • Next.jsをCloud FunctionsののせてSSR
  • REST APIもCloud Functionsを利用する

Next.jsをCloud Functionsにのせる

実はこれはNext.jsのGitHubにサンプルが公開されているので全く難しくありません。
こちらのREADMEに従えば問題なく動作します。

https://github.com/vercel/next.js/tree/canary/examples/with-firebase-hosting

※公開当日は動かなかったんですが、僕が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.jsonscriptsの定義を見ると、それぞれのコマンドの前に色々と処理が入っていることがわかります。

例えば、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);

https://firebase.google.com/docs/functions/http-events?hl=ja

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

こうすることで次のことができるようになります。

  1. /で始まるパスのリクエストはNext.jsがコントロールする
  2. /_/で始まるパスのリクエストはREST APIがコントロールする

これでNext.jsとREST APIをCloud Functionsに共存できるようになりました✨

1amageek
I am a geek. Firebase, Firestore 😎 Firebase Japan User Groupのオーガナイザー 半導体エンジニアから、Timers , Cookpadを経て独立。 新規事業の技術顧問として、あらゆる企業をバックアップさせて頂いております。 お困りごとがあれば何なりとご質問ください。
https://stamp.inc/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away