本記事に記載されている内容は、古いバージョンのSolid Startに基づいていますので、ご注意ください。
Firebase HostingにデプロイしたページにSSGしてみます。
Firebase HostingはSWR(stale-while-revalidate)が使えるようなので、Cloud Functions for Firebaseで得られる値をCDNにキャッシュしてみます。
- SolidJSとSolid Startを使用します。
- パッケージマネージャーにはpnpmを使います。
プロジェクトを作成する
プロジェクトのディレクトリを作成
$ mkdir ssg-firebase-solidjs
$ cd ssg-firebase-solidjs
準備
Blazeプランにする
あとで実際にデプロイして動かしてみます。
Cloud Functions for FirebaseはBlazeプランでなければ実行できないのでBlazeプランにしておく必要があります。
firebase tools をインストール
$ npm install -g firebase-tools
Firebase ログイン
CLIからもfirebaseにログインしていなければ先に進めませんので、以下のコマンドでログインしておきましょう。
$ firebase login
Firebaseを初期化
FunctionsとHostingを有効にして、TypeScriptを使えるようにしましょう。
$ firebase init
動的なデータを生成するバックエンド処理を作成する
Functionsで関数を作成する
モジュールをインストール
$ pnpm -C functions install
@firebase/app-typesがないというエラー(ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies
)が発生した場合は以下でインストールしましょう。
$ pnpm -C functions add @firebase/app-types
関数を作成
functions/src/index.ts
を開いて以下のように書き換えます。
CDNのキャッシュが効いているかどうかをわかりやすくするため、日時を返すようにしています。
import * as functions from "firebase-functions"
export const lastUpdatedAt = functions.https.onRequest((_, response) => {
const dateTime = new Date()
const formattedDate = `${dateTime.getFullYear()}-${dateTime.getMonth() + 1}-${dateTime.getDate()}`
const formattedTime = `${dateTime.getHours()}:${dateTime.getMinutes()}:${dateTime.getSeconds()}`
const json = {
lastUpdatedAt: `${formattedDate} ${formattedTime}`,
}
response.send(json)
})
デプロイする
ビルド
$ pnpm -C functions build
デプロイ
$ firebase deploy --only functions
実行
デプロイが成功すると``のようなURLが表示されるので、Webブラウザーでアクセスしてみましょう。
以下のような表示になるはずです。
ローカルで起動
後ほど、Webサイトをビルドするときにここで作成した関数を実行して結果をWebページに埋め込みます。
ビルド時にローカルで実行できるようにするために、firebase serve
コマンドを使用してローカルでサーバーを起動しておきます。
$ firebase serve -only functions
✔ functions: Using node@16 from host.
i functions: Watching "/path/to/ssg-firebase-solidjs/functions" for Cloud Functions...
✔ functions: Loaded functions definitions from source: lastUpdatedAt.
✔ functions[asia-northeast1-lastUpdatedAt]: http function initialized (http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt).
i functions: Beginning execution of "lastUpdatedAt"
i functions: Finished "lastUpdatedAt" in ~1s
上記ログに表示されている通り、curl http://localhost:5000/ssg-firebase-solidjs/asia-northeast1/lastUpdatedAt
を実行すると、以下のような結果が返ります。
$ curl http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt
{"lastUpdatedAt":"2022-10-9 14:3:26"}
コマンドはCtrl+Cするまで起動したままになるので、これ以降の作業を続けるには別のターミナルを開いてください。
SolidJSでスケルトンコードを作成する
SolidJSの環境を作成
firebase initで作成されたpublicディレクトリは作り直すので、いったん削除します。
$ rm -rf public
続いて同名のディレクトリを作成して、SolidJSのプロジェクトを作成していきます。
$ mkdir public
$ cd public
$ pnpm create solid
テンプレートの選択を要求されます。今回は深く考えずbare
を選択します。
? Which template do you want to use? › - Use arrow-keys. Return to submit.
❯ bare
durable-objects-websocket
hackernews
todomvc
with-auth
with-mdx
with-solid-styled
with-tailwindcss
with-vitest
SSRかどうかと聞かれるのですが、デフォルトでよいのでEnterを押します。
? Server Side Rendering? › (Y/n)
TypeScriptを使うか問われるのでYを押します。
? Use TypeScript? › (y/N)
ディレクトリをプロジェクトのルートに戻しておきます。
$ cd ..
モジュールをインストールします。
$ pnpm -C public install
公開ディレクトリを変更する
ビルドすると、public/build/public
に結果が格納されるのでfirebase.jsonのhosting.public
の設定を以下のように変更しておきます。
"hosting": {
"public": "public/dist/public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
SSGのアダプターを適用する
solid-start-nodeは不要なのでアンインストールします。
$ pnpm -C public uninstall solid-start-node
代わりに solid-start-static をインストールします。これはSSGを実現するアダプターです。
$ pnpm -C public install -D solid-start-static
CDNにキャッシュさせる設定を行う
Functionsの関数の実行にCDNを通す
rewrites設定で、Firebase Hostingを経由させることでCDNを通すようにし、headersで、キャッシュの設定を行っています。
キャッシュする時間は、あとで実験しやすくするために60秒とし、stale-while-revalidateの時間も60秒としています。(つまり120秒間キャッシュの値を返す設定になっている)
"hosting": {
"public": "public/dist/public",
"rewrites": [{
"source": "/lastUpdatedAt",
"function": "lastUpdatedAt",
"region": "us-central1"
}],
"headers": [ {
"source": "/lastUpdatedAt",
"headers": [ {
"key": "Cache-Control",
"value": "public, max-age=60, stale-while-revalidate=60"
} ]
}],
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
静的サイトを作成する
404.htmlのファイルの修正
SSGのためにsolid-start-static
アダプターを使うと、ビルドしたときにエラーが発生してしまうので、public/src/routes/[...404].tsx
の以下をコメントアウトします。
import { HttpStatusCode } from "solid-start/server";
<HttpStatusCode code={404} />
参考: https://pullanswer.com/questions/solid-start-static-adapter-throws-err_invalid_arg_type
index.tsxを書き換える
ビルド時にアクセスするAPIのURLと、実行時にアクセスするAPIのURLを切り替えるようにしていますが、動的に変更したいデータがなければビルド時のAPIだけで良いでしょう。
下記のビルド時のAPIのURLは、ビルド時に実行可能でなければなりません。
onMountはブラウザでしか動作しないのでビルドタイムとランタイムでAPIを分けることができます。
import { createResource, createEffect, onMount } from "solid-js";
type LastUpdatedAtType = {
lastUpdatedAt: string;
}
const BUILD_TIME_API = "http://localhost:5000/ssg-firebase-solidjs/us-central1/lastUpdatedAt";
const RUN_TIME_API = "/lastUpdatedAt";
export default function Home() {
const [api, setApi] = createSignal<string>(BUILD_TIME_API);
const [data, { refetch }] = createResource<LastUpdatedAtType, string>(api, async (url) => (await fetch(url)).json());
onMount(() => setApi(RUN_TIME_API));
return (
<main>
<p>Last updated at: {data()?.lastUpdatedAt || "-"}</p>
<button onClick={() => refetch()}>Refetch!</button>
</main>
);
}
デプロイする
先に hosting のコンテンツをビルドしてから、firebaseにデプロイを行う。
$ pnpm -C public build
$ firebase deploy --only hosting
動作確認
Webブラウザーで表示すると以下のような表示になります。
SSGのため、ビルド時にlastUpdatedAtを実行した結果が埋め込まれた状態になるので、日時が表示されます。
SWRを確認してみる
ビルドしてから60秒以上経っている前提ですが、画面を表示してRefresh!
ボタンを押します。
それから60秒経過するまでの間、なんどRefresh!
ボタンを押しても日時は変わりません。
Cloud Functions for Firebaseの関数は実行されず結果をCDNのキャッシュから返しているためです。
60秒経過して、120秒経過するまでにRefresh!
ボタンを押しても日時に変更はありません。
そして、120秒経ってもう一度Refresh!
ボタンを押すと日時は変わります。しかし、このときに表示される日時はボタンを押した日時ではありません。60秒経過して、120秒経過するまでにRefresh!
ボタンを押した時の日時が表示されます。
上記の通りになればSWRによって日時がフェッチされたことが確認できたと言っていいと思います。
最後に
いかがでしたでしょうか。Firebase HostingでSolidJSを使いSSGを実現してみただけでなく、SWRによるCDNのキャッシュの動きも確認してみました。
なかなか前衛的なモジュールを使っての試みだったので、いつまで本ページの内容が実験可能であるか保証はできませんが、興味のある方はトライしてみてください。
一応、package.jsonの中身も記載しておきます。
functions
{
"name": "functions",
"scripts": {
"lint": "eslint --ext .js,.ts .",
"build": "tsc",
"build:watch": "tsc --watch",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "16"
},
"main": "lib/index.js",
"dependencies": {
"@firebase/app-types": "^0.8.0",
"firebase-admin": "^10.0.2",
"firebase-functions": "^3.18.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^0.2.0",
"typescript": "^4.5.4"
},
"private": true
}
hosting
{
"name": "public",
"scripts": {
"dev": "solid-start dev",
"build": "solid-start build",
"start": "solid-start start"
},
"type": "module",
"devDependencies": {
"solid-start-static": "^0.1.5",
"typescript": "^4.8.3",
"vite": "^3.1.0"
},
"dependencies": {
"@solidjs/meta": "^0.28.0",
"@solidjs/router": "^0.5.0",
"solid-js": "^1.5.7",
"solid-start": "^0.1.0",
"undici": "^5.10.0"
},
"engines": {
"node": ">=16"
}
}
以上です〜!