3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

モノレポでVueのSPA × HonoのAPI の技術スタックでCloudflare PagesにDeployし、フルスタックな開発する

Last updated at Posted at 2024-09-08

はじめに

Cloudflare Pagesでは静的なコンテンツは無制限に配信できる(すごいですよね)。また、Cloudflare Pages Functionsを利用すればエッジ上でJavaScriptを実行でき、バックエンドのサーバーを用意することもできる。

今回はそんなCloudflareのスタックに乗っかり、簡単にフロントエンドはSPA(Vue.js)で実装して静的なコンテンツとして配信、バックエンドはHonoでAPIを実装、というのをやってみたいと思う。

要件は以下。

  • /api/*以外はSPA(静的なコンテンツの配信)として処理されるようにし、/api/*のみPages Functionsで処理する
    image.png

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は以下のようになった。

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を以下のように書き換える。

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のディレクトリの設定
    • Honoを一からセットアップ(CLIツールで作成)した後のプロジェクトはsrc/以下にHonoのコードが配置されるような構成になるが、Vueの方でそれは使っているのでsrv/以下にしている
    • srv/以下がエントリーになるように指定するために、entryオプションを設定している
  • resolve.aliasの設定をHonoの設定にも追加する
    • これはaliasを利用している場合のみに関係してくるが、Vueの方でこの設定があるとviteの仕組み上エラーになるため、Honoの方でもこの設定が必要

このように設定することで、開発時は以下のようなコマンドでそれぞれのdevServerを起動でき、ビルドもそれぞれできるようになる。

package.json
   "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 modeCreate 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に振り向けてしまうから。

_routes.json
{ "version": 1, "include": ["/*"], "exclude": ["/assets/*", "/favicon.ico", "/index.html"] }

これを要件に合うように変更する必要があるが、 includeの部分がポイントで、"/*"の状態だとすべてのリクエストがPages Functionsに振り向けられるため、ここを"/api/*"に上書きしてしまえば、/api/*のパスのみPages Functionsに振り向けられるので、それ以外のリクエストをすべて静的なコンテンツの配信として処理できるようになる。

_routes.json
{"version":1,"include":["/api/*"],"exclude":["/assets/*","/favicon.ico","/index.html"]}

これをビルドのプロセスでどうやるか?だが、以下のようにビルドコマンドを定義してしまえばできる。単純にシェルスクリプトで置き換える。この方法であれば、Cloudflare PagesのDeploy時の設定でyarn buildを指定すれば、_routes.jsonの編集までできるので、今回やりたかったことが実現できる。

package.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",
	},

image.png

まとめとして

今回はVue × HonoをCloudflare PagesにDeployして、フルスタックな開発をできるようにする方法を見てきた。今どきはSSRが多いと思うが、要件によってはSPAの方があうものもあったりすると思う。そんなSPAでバックエンドを用意したくなったときに、Firebaseなどでは物足りず、APIを実装したくなったときにHonoを利用してAPIだけ開発し、それを簡単にDeployできる技術スタックは有用な気もした。

どなたかの役に立てば幸いです。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?