Firebase + Vue.js の構成で SSR (ServerSideRendering) をする実例や紹介記事はそれなりに出てきているが、たいていは Nuxt.js を使ったもの。今回は Vue.js をベースにしたフレームワーク QuasarFramework を使って Firebase で SSR する構成例を紹介する。
QuasarFramework v0.17.0 は SSR に対応しているので、それをうまいこと Cloud Functions で動かすようにすれば良い。しかも QuasarFramework の SSR は内部的には vue-server-renderer
と express
を使っているだけで、特別なことはしていない。つまり Nuxt.js と同じようなアプローチになる。
今回のデモとソース
QuasarFramework の SSR 対応
QuasarFramework で SSR に対応するには前回書いた記事があるので、それを参照してほしい。
Cloud Functions の作成
Cloud Functions の中に QuasarFramework でビルドした SSR 用のファイルを含める必要がある。そのためビルド先を ./functions
以下に変更する。
quasar.conf.js
QuasarFramework のビルド先を functions/dist
に指定。
module.exports = function (ctx) {
return {
build: {
scopeHoisting: true,
vueRouterMode: 'history',
distDir: 'functions/dist', // 追加
}
}
}
functions/index.js
続いて Cloud Functions を作っていく。
const functions = require('firebase-functions');
const server = require('./server/server.js');
exports.server = functions.https.onRequest(server.handler);
functions/server/server.js
これは QuasarFramework を SSR モードでビルドしたときに自動生成される src-ssr/index.js
をほぼコピーして持ってきている。変更した点といえばビルドファイルの読み込みパスと express のサーバ起動をやめてアプリケーションを export しているくらい。
const
express = require('express'),
compression = require('compression')
const
ssr = require('../dist/ssr'),
app = express(),
port = process.env.PORT || 3000
const serve = (path, cache) => express.static(ssr.resolveWWW(path), {
maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0
})
// gzip
app.use(compression({ threshold: 0 }))
// serve this with no cache, if built with PWA:
if (ssr.settings.pwa) {
app.use('/service-worker.js', serve('service-worker.js'))
}
// serve "www" folder
app.use('/', serve('.', true))
// this should be last get(), rendering with SSR
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html')
ssr.renderToString({ req, res }, (err, html) => {
console.log('error', err)
if (err) {
if (err.url) {
res.redirect(err.url)
}
else if (err.code === 404) {
res.status(404).send('404 | Page Not Found')
}
else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
if (ssr.settings.debug) {
console.error(`500 on ${req.url}`)
console.error(err)
console.error(err.stack)
}
}
}
else {
res.send(html)
}
})
})
exports.handler = app;
functions の package
QuasarFramework の SSR には色々なパッケージに依存しているので、それを Cloud Functions で使えるようにインストールする。
cd functions
npm install --save @babel/runtime axios compression core-js express \
lru-cache regenerator-runtime vue vue-i18n vue-router vue-server-renderer vuex
デプロイスクリプト
QuasarFramework のビルドで生成される package.json
は必要ないため削除する。手動で削除するのが面倒くさいので、ビルドスクリプトを用意する。
#!/usr/bin/env bash
quasar build -m ssr
rm -rf ./functions/dist/package.json
firebase deploy
Firebase の rewrite 設定
あとは Firebase にアクセスした際に Functions へ引き渡すように設定すれば完了。
firebase.json
public
を QuasarFramework がビルドする静的ファイルのディレクトリ functions/dist/www
を指定。 rewrites
でアクセスを server
function に渡すように設定。
{
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
]
},
"hosting": {
"public": "functions/dist/www",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "server"
}
]
}
}
これでデプロイが無事完了すれば SSR されるはず。
Google クローラーの確認
Google SearchConsole で fetch してもらった HTML を念の為確認する。SSR しているものと通常の Ajax でレンダリングしたものを分けるために以下のように実装したもので試す。
<template>
<q-page padding>
<qo-menu></qo-menu>
<h1>Posts page</h1>
<h2>SSR contents</h2>
<ul>
<li v-for="(tag, index) in getTags" :key="index">
{{tag.id}}
</li>
</ul>
<h2>Ajax contents</h2>
<ul>
<li v-for="(post, index) in getPosts" :key="index">
{{post.id}}: {{post.title}}
</li>
</ul>
</q-page>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import QoMenu from '../components/QoMenu.vue'
export default {
components: {
QoMenu
},
preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
return store.dispatch('post/fetchTags')
},
computed: {
...mapGetters('post', [
'getTags',
'getPosts'
])
},
methods: {
...mapActions('post', [
'fetchZaruPosts'
])
},
mounted () {
this.fetchZaruPosts()
}
}
</script>
<style>
</style>
preFetch
で取得したデータは SSR の対象になるが、 mounted
などで取得しているデータは SSR はされない。つまり一般的な SPA などと変わらない。
これを Google SearchConsole から手動クロールした結果の HTML を確認する。
細かくてわかりにくいが preFetch
で取得したデータに関してはちゃんと HTML としてレンダリングされ Google のクローラにも認識されている。Good!
リンクを <a>
タグにする
あとは Google クローラーが巡回しやすいように重要な動線を <a>
タグを使い href
で URL 指定すれば OK 。VueRouter であれば <router-link>
を使えば良い。QuasarFramework は <router-link>
をラップしたコンポーネントがたくさんあるので同じようにオプションを指定する。
<router-link :to="{ name: 'home' }">Home</router-link>
<q-item link tag="a" :to="{ name: 'Home' }">
<q-item-main>
Home
</q-item-main>
</q-item>
こうすることで以下のようにレンダリングされる。
<a href="/">Home</a>
このようにしておかないと、せっかくクロールされても単体の孤立したページとして認識されてしまうので気をつけたい。
Cloud Functions をキャッシュする
Cloud Functions はしばらくアクセスされないとインスタンスが削除されスリープ状態になる。つまり遅い。SEO やユーザ体験的に SSR したコンテンツの表示が遅いのは困るので、キャッシュさせることにする。幸いなことに Cloud Functions は Firebase Hosting の CDN に対応しているので、単純にキャッシュコントロールを指定すれば良い。キャッシュされては困るようなものは preFetch
ではなく通常の Ajax で取得するようにすれば良い。
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html')
res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=600')
// 略
})
これで600秒間のキャッシュが作成され高速にレスポンスされる。最高。