そーす

I'm a programmer in Fukuoka. Please contact me saubre.app[at]gmail.com or Twitter DM.

Next.js+FirebaseHostingで構築するサーバレスWebアプリケーション

github.com

Next.jsというReactアプリケーションをデフォルトでServerSideRenderingしてくれるライブラリがあります。

これをFirebaseHosting上にFirebaseFunctionsを使って構築することで、無料でサーバレスSPAを作る事ができます。

Next.jsをclone

Next.jsはサンプルがとても豊富です。

github.com

今回使うサンプルはwith-firebase-hostingです。

github.com

cloneしたらwith-firebase-hostingディレクトリをコピーして使いましょう。

Hello World

まずは依存ライブラリをインストールします。

npm i

そして

npm run next

でローカルサーバが立ち上がります。

デフォルトではlocalhost:3000ですね

f:id:saburesan:20171114103843p:plain

これは単にnextjsを起動しているだけで、Firebaseは全く関係ありません。

デプロイ

事前にFirebaseでプロジェクトを作っておいてください。

プロジェクトを作ったら.firebasercの<project-name-here>をプロジェクト名に置き換えます

// .firebaserc
{
  "projects": {
    "default": "<project-name-here>"
  }
}

これでFirebaseの設定は完了です。

npm run deploy

これで設定したプロジェクトにデプロイされます。

(npm run serveは恐らく動かないと思います。)

デプロイが完了するとアップデート先のURLがログに出るのでアクセスしてみると、

先程ローカルで動かした内容が表示されてるかと思います。

もし404や500が出た場合はFirebase consoleのFunctionsでログを見ることができるので確認して見てください。

f:id:saburesan:20171114110616p:plain

プロジェクトの構造

app

実際にコードを書いていくディレクトリです。

nextjsのルートディレクトリです。

普段使うnextjsと違うのはnext.config.jsファイルでコンパイルの保存先がfunctions/nextになっているところくらいでしょうか。

functions

デフォルトではこのディレクトリがFirebaseにデプロイされます。

appのコンパイル先でもあります。

public

これはFirebaseHostingのルートとなるディレクトリです。

placdholder.htmlというのが入ってますが、基本的に触ることはないです。

FirebaseHostingの設定でpublicは必須です。

存在していなければデプロイに失敗します。

Deployment Configuration  |  Firebase

firebase.json

Firebaseの細かい設定を書きます。

{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "**/**",
        "function": "next"
      }
    ]
  },
  "functions": {
    "source": "functions"
  }
}

デフォルトでは上のようになっています。

hostingの設定ですが、publicは必須でrewritewですべてのアクセスに対してFirebaseFunctionsのnext関数を起動するようになっています。

functionsの設定でFirebaseFunctionsのソースを指定しています。functions/index.jsの内容は

const functions = require('firebase-functions')
const next = require('next')

var dev = process.env.NODE_ENV !== 'production'
var app = next({ dev, conf: { distDir: 'next' } })
var handle = app.getRequestHandler()

exports.next = functions.https.onRequest((req, res) => {
  console.log('File: ' + req.originalUrl) // log the page.js file that is being requested
  return app.prepare().then(() => handle(req, res))
})

となっていて、exports.nextでnextをエクスポートしています。

writesの"function":"next"はこのnextを呼んでいます。

面倒なところ

デフォルトではfunctionsがデプロイされますが、functionsに含まれるappのコンパイルの成果物distはpagesに紐づくファイルだけなので、

例えばapp/package.jsonのdependenciesはfunctions/package.jsonに書き足して上げないといけません。

画像を扱う場合はapp/staticというフォルダに入れて使うのがnextjsでは一般的なのですが、このstaticもコピーしなければなりません。

他にも、起動サーバーをカスタマイズする場合などもコピーして…

という感じになってしまいます。

appにまとめる

一応、今私はfunctionsの内容をappに移して運用しています。

そうなるとデプロイに不要なファイルが多く含まれるので、firebase.jsonのignoreで指定しています。

app/index.jsはfunctionsのindex.jsをそのまま使い、サーバーをカスタマイズする場合は

exports.start = () => {
  app.prepare().then(() => {
    createServer((req, res) => {
      ...
    }).listen(port, (err) => {
      if (err) throw err;
      console.log(`> Ready on http://localhost:${port}`);
    });
  });
};

という感じにしました。

package.json

"scritps" : {
  "dev": "node -e \"require('./').start()\"",
  ...
}

"dependencies": {
  "firebase-admin": "^5.5.0",    // 追加
  "firebase-functions": "^0.7.3", // 追加
  ...
}

firebase.json

{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "**/**",
        "function": "next"
      }
    ],
    "ignore": [
      "**/.*",
      "**/node_modules/**",
      "app/src",
      "app/flow-typed",
      "app/pages"
    ]
  },
  "functions": {
    "source": "app"
  }
}

package.json追記したりstaticコピーしてくるよりこっちのほうがいいなーと思いました。

よくわからない問題

firebase serveが上手く動かない

404だったり、変更が全然反映されなかったり…

謎です。

ローカルでは問題無く動くが、デプロイすると動かない

デプロイするとstyled-jsxとbabel-runtimeが無いというエラーが出ます。

ローカルではnode_modulesに入っているのですが…

追加でインストールすると動くようになりました。

まとめ

ハマリポイントありますが、無料でWebアプリ作れるのは良いですね〜

ちなみに、github pagesだった自分のポートフォリオをNext.js+FirebaseHostingに移行しました。

ryo-hlan.firebaseapp.com

サーバレスなんで、アクセスの感覚が空くとやっぱり遅いけどインスタンスが起動しているときなら速度は問題無さそう。

f:id:saburesan:20171114164328p:plain

まぁコンテンツ少ないんでね…

そのうちブログも移行します。