はじめに
Cloudflare Pagesでは静的なコンテンツは無制限に配信できる(すごいですよね)。また、Cloudflare Pages Functionsを利用すればエッジ上でJavaScriptを実行でき、バックエンドのサーバーを用意することもできる。
今回はそんなCloudflareのスタックに乗っかり、簡単にフロントエンドはSPA(Vue.js)で実装して静的なコンテンツとして配信、バックエンドはHonoでAPIを実装、というのをやってみたいと思う。
要件は以下。
※HonoでAPI付き雑React SPA最小、HonoでAPIだけ作って素のReact DOMでSPAを書くアーキテクチャなど、似たようなことを扱っている記事はあるが、それらはSPAの配信の部分をHonoの中で行う実装になっている。つまり、PagesでSPAを配信するのではなく、Pages Functionsで処理してSPAを返すようになっている。Pages Functionsは無料枠では処理できるリクエストの上限があるし、課金でもリクエスト回数で課金されるので、SPAはPages Functionsにリクエストが届かずとも配信できるようにしたいと考えた。今回やってみることは、Honoは完全にAPIの処理のみを行い、SPAはPagesで配信するという事になる。
SPAの設定
まずは、create-vueで一からプロジェクトを作成する。私はVuetifyまで入れたので最終的には、vite.config.js
は以下のようになった。
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import vue from '@vitejs/plugin-vue';
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
import Components from 'unplugin-vue-components/vite';
import vueDevTools from 'vite-plugin-vue-devtools';
import eslint from 'vite-plugin-eslint';
export default defineConfig(() => {
return {
plugins: [
nodePolyfills({ protocolImports: true }),
vue({ template: { transformAssetUrls } }),
vuetify({
autoImport: true,
styles: { configFile: 'src/styles/settings.scss' }
}),
Components(),
vueDevTools(),
eslint()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
watch: 'src/**',
host: '0.0.0.0',
port: 8080
}
};
});
フォルダ構成はそれこそ普通のVueのプロジェクトの通りになる(開発しているアプリに関連するSFCの名前になっている)。
$ tree src/
src/
├── App.vue
├── assets
│ └── logo.svg
├── main.js
├── plugins
│ ├── index.js
│ └── vuetify.js
├── router
│ └── index.js
├── stores
│ ├── problem.js
│ └── setting.js
├── styles
│ └── settings.scss
└── views
├── HomeView.vue
├── ProblemDetailView.vue
└── ProblemsView.vue
SPAの方はこれで特におしまい。
続いてHonoの設定を行っていく。
HonoでAPIを実装する
Cloudflare PagesはNode.jがランタイムではないので、ブラウザ環境と同じようにviteでビルドを行う必要がある。そこでviteの設定が必要になる。
Honoも初期プロジェクトを作成できるCLIツールがあり、それを利用すれば簡単に設定できるが、今回はSPAのプロジェクトに同居させるので、それは利用しない。vite.config.js
を以下のように書き換える。
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import vue from '@vitejs/plugin-vue';
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
import Components from 'unplugin-vue-components/vite';
import vueDevTools from 'vite-plugin-vue-devtools';
import eslint from 'vite-plugin-eslint';
// for hono
import build from '@hono/vite-cloudflare-pages';
import devServer from '@hono/vite-dev-server';
import adapter from '@hono/vite-dev-server/cloudflare';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// for hono
if (mode === 'server')
return {
plugins: [
nodePolyfills({ protocolImports: true, globals: { Buffer: true } }),
build({ entry: 'srv/index.js' }),
devServer({
adapter,
entry: 'srv/index.js'
})
],
// Configuration for avoiding the following errors
/**
* Error: The following dependencies are imported but could not be resolved:
* @/plugins (imported by /home/study/src/main.js)
* Are they installed?
*/
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: { host: '0.0.0.0', port: 3000, watch: 'srv/**' }
};
// for vue
return {
plugins: [
nodePolyfills({ protocolImports: true }),
vue({ template: { transformAssetUrls } }),
vuetify({
autoImport: true,
styles: { configFile: 'src/styles/settings.scss' }
}),
Components(),
vueDevTools(),
eslint()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'^/api': {
target: 'http://192.168.56.5:3000'
}
},
watch: 'src/**',
host: '0.0.0.0',
port: 8080
}
};
});
ポイント以下の部分になる。
-
mode
の利用- ここはHonoでAPI付き雑React SPA最小、HonoでAPIだけ作って素のReact DOMでSPAを書くアーキテクチャの記事を参考にさせて頂いた
- Vue(SPA)はVueでビルトをする必要があり、HonoはHonoでビルドをする必要があるが、それをうまく1つのプロジェクト(モノレポ)で実現するために、viteの
mode
を利用してビルドする対象ごとに設定が変わるようにしている - Vueのビルドは
$ vite build
で、Honoのビルドは$ vite build --mode server
になる
- Honoのディレクトリの設定
- Honoを一からセットアップ(CLIツールで作成)した後のプロジェクトは
src/
以下にHonoのコードが配置されるような構成になるが、Vueの方でそれは使っているのでsrv/
以下にしている -
srv/
以下がエントリーになるように指定するために、entry
オプションを設定している
- Honoを一からセットアップ(CLIツールで作成)した後のプロジェクトは
-
resolve.alias
の設定をHonoの設定にも追加する- これはaliasを利用している場合のみに関係してくるが、Vueの方でこの設定があるとviteの仕組み上エラーになるため、Honoの方でもこの設定が必要
このように設定することで、開発時は以下のようなコマンドでそれぞれのdevServerを起動でき、ビルドもそれぞれできるようになる。
"scripts": {
"dev": "vite",
"dev:server": "vite --mode server",
"build": "vite build && yarn build:server && yarn replace:routes",
"build:server": "vite build --mode server"
},
Honoのディレクトリ構成は以下のようになる。
$ tree ./srv
./srv
├── index.js
└── lib
├── http-error.js
└── jwt.js
最後に、Cloudflare PagesにDeployするための設定についてみていく。
Cloudflare PagesにDeployするための設定
上記で開発とビルドまでできるようになったが、今回の要件として、VueはPagesの静的コンテンツの配信にし、APIはPages Functionsにする、というのがある。そのための設定を行っていく。
まず、Cloudflare PagesにDeployするときのフォルダ構成・ファイルのルールを理解する必要があるので、それについてみていく。
まず、基本的にCloudflare Pages FunctionsはCreate a Functionに書かれている通り、/functions
というフォルダ以下にファイルを配置すると、それがPages FunctionsとしてDeployされるような仕組みになっている。つまり、Pagesの設定でビルド後の生成物のディレクトリを./dist
としたなら、以下のようなフォルダ構成にすると、自動的にfuncions以下をPages Functionsと認識してくれるという事。
$ tree ./dist/
./dist/
├── assets
│ ├── index-BXVXHimq.js
│ └── index-D8XTPsNk.css
├── favicon.ico
├── functions
│ ├── helloworld.js
│ └── index.js
└── index.html
ただ、HonoをはじめとしたCloudflare Pages Functions向けのビルドを行うもの(アダプター)は、このフォルダ構成でPages Functionsを作成できる方法を利用しない。代わりに、_worker.js
を生成して_routes.json
によるルーティング設定を行う(Advanced mode、Create a _routes.json fileを参照)。
実際にHonoをビルドすると、以下のように_worker.js
と_routes.json
が生成される。
$ tree ./dist/
./dist/
├── _routes.json
├── _worker.js
├── assets
│ ├── index-BXVXHimq.js
│ └── index-D8XTPsNk.css
├── favicon.ico
└── index.html
ただ、このままだと今回の要件は満たせない。理由は自動的に生成される_routes.json
が以下のようになっており、すべてのリクエストをPages Functionsに振り向けてしまうから。
{ "version": 1, "include": ["/*"], "exclude": ["/assets/*", "/favicon.ico", "/index.html"] }
これを要件に合うように変更する必要があるが、 include
の部分がポイントで、"/*"
の状態だとすべてのリクエストがPages Functionsに振り向けられるため、ここを"/api/*"
に上書きしてしまえば、/api/*
のパスのみPages Functionsに振り向けられるので、それ以外のリクエストをすべて静的なコンテンツの配信として処理できるようになる。
{"version":1,"include":["/api/*"],"exclude":["/assets/*","/favicon.ico","/index.html"]}
これをビルドのプロセスでどうやるか?だが、以下のようにビルドコマンドを定義してしまえばできる。単純にシェルスクリプトで置き換える。この方法であれば、Cloudflare PagesのDeploy時の設定でyarn build
を指定すれば、_routes.json
の編集までできるので、今回やりたかったことが実現できる。
"scripts": {
"build": "vite build && yarn build:server && yarn replace:routes",
"build:server": "vite build --mode server",
"replace:routes": "echo '{\"version\":1,\"include\":[\"/api/*\"],\"exclude\":[\"/assets/*\",\"/favicon.ico\",\"/index.html\"]}' > dist/_routes.json",
},
まとめとして
今回はVue × HonoをCloudflare PagesにDeployして、フルスタックな開発をできるようにする方法を見てきた。今どきはSSRが多いと思うが、要件によってはSPAの方があうものもあったりすると思う。そんなSPAでバックエンドを用意したくなったときに、Firebaseなどでは物足りず、APIを実装したくなったときにHonoを利用してAPIだけ開発し、それを簡単にDeployできる技術スタックは有用な気もした。
どなたかの役に立てば幸いです。