FirebaseでSPAのSSR環境をサーバーレスに構築する
Firebase HostingとCloud Functions for Firebaseがすごく便利!ってことで今回はそれらを使ってSPAのSSR環境をサーバーレスで実現する方法をご紹介します。
SPAの実装にはNuxt.jsを使いたいと思います。Nuxtってなに?という方はこちらの記事を参考にしてください。
Firebase Hostingについて
Firebase Hosting はデフォルトでHttps対応やCDN対応をしてくれている静的コンテンツのHostingサービスです。カスタムドメインの接続もダッシュボード上で簡単にできます。これだけでもAWS S3より使いやすかと僕は思いますが、それに加えてCloud Functions for Firebaseなどのサーバーレスコンピューティングと統合することができます。その結果以下のケースを実現することができるようになりまいす。
- 動的コンテンツの配信
Hosting サイトでの静的コンテンツの配信に加えて、サーバー側ロジックを実行する関数から動的に生成されたレスポンスを提供できます。
- 動的コンテンツのキャッシュ
動的に生成されたコンテンツをキャッシュに保存することでアプリを高速化できます。そのコンテンツはCDN から配信されるため実行コストも削減できます。
- SPAの事前レンダリング
動的な meta
タグを事前作成することでSEO を改善します。またSPAの弱点である初回ロード時のブラウザ側のレンダリングコストの削減できます。
- REST APIのビルド
あるルーティングへのリクエストをリダイレクトさせることでAPIサーバーの役割を担うことができます。たとえば、/api
へのリクエストをすべてFunctions にリダイレクトさせることができます。
今回は動的コンテンツの配信とキャッシュ、事前レンダリングのチュートリアルとしてサーバーサイドで各ページのOGP設定を変更するSPAを構築してみようと思います。
チュートリアル
デモサイトはこちらにて公開しています。
ソースコードはhttps://gitlab.com/KazuyaFujimori/nuxt-firebase-sample/tree/feature/demoにおいています。詳細はこちらを参考にしてください。
- プロジェクトディレクトリの作成
$ mkdir <new-project> && cd $_
またプロジェクト内にSPAのソースフォルダを/srcとして作成します。そのフォルダに移動しましょう。
$ mkdir src && cd $_
2. Nuxtでサーバサイド、フロントサイドのコードを書きます
$ create-nuxt-app
僕の設定はこんな感じです。
src/pages/index.vueとsrc/pages/other.vueを作成して、サーバーサイドでmetaタグを設定するためのコードを加えます。実際のアプリではasyncDataの中でAPIを呼び出しそのレスポンスデータを使用してレンダリングするケースが多いかと思いますが、このチュートリアルでは一旦静的な値とします。
<script>export default {
async asyncData({ params }) {
const image = <ページ毎に任意に設定してください>;
return { image };
},
head() {
return {
title: "Top",
meta: [
{ hid: 'og:type', property: 'og:type', content: 'website' },
{ hid: 'og:title', property: 'og:title', content: 'SSR Sample' },
{ hid: 'og:description', property: 'og:description', content: 'Top Top Top' },
{ hid: 'og:image', property: 'og:image', content: this.image },
]
};
}
}
</script>
3. Firebase Functionsを使用してSSRを実行します
まずBrowserのFirebase Consoleからプロジェクトを作成します。
Cloud Functionsを制限なく実行するには料金プランをFlame(月固定$15)かBlaze(従量課金)に設定する必要があります。本格的なプロジェクトでの使用でなければBlazeの毎月の無料枠の範囲内でサービスを利用できると思います。
次にFirebase CLIをインストールしましょう。
$ npm install -g firebase-tools
次にプロジェクトディレクトリに戻り、Firebase functionsの初期設定をおこないます。
$ cd ../$ firebase init functions
ここで
? Select a default Firebase project for this directory:
とコンソール上で聞かれるのでさきほど作成したプロジェクトを選択します。
functionsフォルダが作成されているので、そこでpackge.jsonにexpressとSPAで使用したモジュールを全て追加します。この時srcディレクトリで使用したモジュールとバージョンをあわせてください。そしてNuxtを動かすためにはNodeのVersion8以降の環境が必要なのでこちらもpackage.json内で指定します。僕のpackage.jsonはこんな感じになりました。
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"engines": {
"node": "8"
},
"scripts": {
"lint": "eslint .",
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"express": "^4.17.1",
"firebase-admin": "~7.0.0",
"firebase-functions": "^2.2.0",
"nuxt": "^2.4.0",
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/pwa": "^2.6.0"
},
"devDependencies": {
"eslint": "^5.12.0",
"eslint-plugin-promise": "^4.0.1"
},
"private": true
}
package.jsonを編集したらモジュールをインストールしてください。
$ yarn
そしてSSR用のコードをfunctions/index.jsに記述します。これはNuxtがプログラムから利用できることを利用しています。詳しくはNuxtの公式ドキュメントを参照してください。
const functions = require(‘firebase-functions’)
const express = require(‘express’)
const { Nuxt } = require(‘nuxt’)const app = express()const config = {
dev: false,
buildDir: ‘nuxt’,
build: {
publicPath: ‘/’
}
}
const nuxt = new Nuxt(config)function handleRequest(path, req, res) {
res.set(‘Cache-Control’, ‘public, max-age=600, s-maxage=1200’)
nuxt.renderRoute(path, { req }).then(result => {
return res.send(result.html)
}).catch(e => {
console.error(e)
return res.send(e)
})
}function handleRoot(req, res) {
handleRequest(‘/’, req, res)
}function handleSample(req, res) {
handleRequest(‘/sample’, req, res)
}app.get(‘/sample’, handleSample)
app.get(‘*’, handleRoot)
exports.ssrapp = functions.https.onRequest(app)
functions/index.js内に以下のような記述をしました。
res.set(‘Cache-Control’, ‘public, max-age=300, s-maxage=600’)
これはキャッシュの設定をしています。キャッシュタイムはブラウザでは300秒、CDNレベルでは600秒となります。CDNにリクエストがキャッシュされている間は同じエリアからの同様のリクエストに対してサーバーの代わりにCDNからがレスポンスを返すことになります。これはクライアントの読み込み時間とサーバーの実行コストの削減に繋がります。
Firebaseではバックエンドコードが処理するレスポンスはデフォルト設定ではキャッシュに保存されないようになっています。
4. Firebase Hostingを使用して静的アセットをホスティングします
$ cd ../$ firebase init hosting
ここでプロジェクトフォルダにpublicというフォルダが作成されます。このフォルダ内のファイルが静的アセットとして配信させるようになります。
SSRをするためにリクエストをfunctionsに送る設定をfirebase.jsonに加えます。
{
"functions": {
"source": "functions"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "ssrapp"
}
]
}
}
5. デプロイ
デプロイのためにNuxtのビルドを行います。
nuxt.config.jsにビルド先のフォルダを先ほどのfunctions/index.jsでconfig内に記述したフォルダに指定します。また今回はルートにデプロイするのでpublicPathをルートに設定します。
buildDir: ‘../functions/nuxt’,
build: {
publicPath: '/',
}
そして下記コマンドを実行してビルドしましょう。
$ yarn build
結果としてfunctions/nuxtフォルダ内にビルドしたファイルが生成されます。functions/nuxt/distというフォルダ内のファイルが配信するためコードになります。このdistフォルダをみてみるとclientフォルダとserverフォルダが存在しています。そしてこのclientフォルダがクライアントに配信するコード、サーバーサイドで実行するコードになります。
パフォーマンス最大化のためにFirebase Hositngで静的ファイルをホスティングしたいので、クライアント用のコードfunctions/nuxt/dist/clientをpublic/にコピーします。ここにさきほど作成した静的ファイルをコピーします。staticに配置している静的アセットもpublicにコピーしておきます。
$ rm public/404.html public/index.html
$ cp -R functions/nuxt/dist/client/ public && cp -R src/static/* public
最後にCLIをつかってデプロイしましょう。
$ firebase deploy
コンソールにデプロイされたURLが表示されると思います。URLの各ルート(/, /other)のOGPが異なっていることを確認してみてください。SSRができていることを確認できると思います。
最後に
まだβですが、Cloud RunというコンテナアプリケーションもHostingと統合できるみたいで、Go, Python, JavaなどDockerfileがサポートする言語を使用できるようです。時間があればそちらも試してみたいと思います。