最近は多くのサービスでビルドしたWebアプリケーションがvercelやaws-amplifyのサービスによって簡単にデプロイさせることができます。
しかし、例えばNode.js サーバーでWebAPIを構築してその検証や内部利用のみの簡単なWebアプリケーションを同じサーバーから配信したかったりすることがあるのではないかと思います。
そこで忘備録としてSPAをNode.jsサーバーから配信するためにどのような設定が必要なのか見ていきたいと思います。
今回はHonoを用いたシンプルなサーバーと、viteでビルドするweb アプリケーションを例に見ていきたいと思います。こな気はreactですが、vueでもsvelteでも同様です。
ただビルドをサーブしてみる
まずはうまくいかないケースとその原因を見ていきたいと思います。
ReactでビルドしたアプリをHonoからサーブしてみます
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
build: {
outDir: '../../dist/react-app'
},
plugins: [react()],
})
vite.config.ts
ではデフォルトの設定に対してbuildのoutDirを変更しています。
今回はVueとReactの二つのビルドがあるためビルドの保存先を分ける必要があるからです。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { html } from 'hono/html'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Hono()
app.get('/react-app', async (c) => {
const file = path.join(__dirname, '../dist/react-app/index.html')
const content = await fs.promises
.readFile(file, 'utf-8')
c.header('Content-Type', 'text/html')
return c.html(content)
})
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
})
Honoではreactのアプリケーションを /reacct-app
のパスへのリクエストで返すようにしました。
ビルドされたhtmlファイルを取得し返します。
結果
ブランクな画面のままで何も表示されません。
エラーを見てみるとサーバーで設定していないパスにjsやcssファイルがリクエストされており404 not foundエラーとなっています。
Viteのビルドを見直す
viteビルドしたアプリケーションのHTMLがHonoのサーバーのパスと合致していないのが原因です
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<script type="module" crossorigin src="/assets/index-kT8-kBJj.js"></script> <!-- unmatching path -->
<link rel="stylesheet" crossorigin href="/assets/index-D8b4DHJx.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
vite.config.ts
でアプリケーションのベースとなるURLを変更しましょう
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
base: '/react-app/', // base url has changed to match with the hono server
build: {
outDir: '../../dist/react-app'
},
plugins: [react()],
})
結果2
まだブランクなままで表示されません。
jsやcssのパスは /react-app...
で合致していますが、今度はHono側で /react-app
ルートのベースURLにしかサーバーのパスを設定していないため404エラーとなってしまっています。
Honoで静的アセットへのリクエストをハンドリングする
簡易な対応にはなりますが、/react-app
以降のリクエストを静的に返すようにHonoにミドルウェアを追加します
import { serve } from '@hono/node-server'
import { serveStatic } from 'hono/serve-static' // added
import { Hono } from 'hono'
import { html } from 'hono/html'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = new Hono()
app.get('/react-app', async (c) => {
const file = path.join(__dirname, '../dist/react-app/index.html')
const content = await fs.promises
.readFile(file, 'utf-8')
c.header('Content-Type', 'text/html')
return c.html(content)
})
app.use('/react-app/*', serveStatic({ // added
root: '../dist',
getContent: async (filePath, c) => {
try {
const content = await fs.promises.readFile(path.join(__dirname, filePath), 'utf-8');
const ext = path.extname(filePath);
let contentType = 'text/plain';
if (ext === '.js') {
contentType = 'application/javascript';
} else if (ext === '.css') {
contentType = 'text/css';
} else if (ext === '.html') {
contentType = 'text/html';
} else if (ext === '.svg') {
contentType = 'image/svg+xml';
} else {
console.log('Unknown file type:', ext);
contentType = 'text/plain';
}
c.header('Content-Type', contentType);
return new Response(content);
} catch (e) {
console.error(e);
return null;
}
}
}))
serve({
fetch: app.fetch,
port: 3000
}, (info) => {
console.log(`Server is running on http://localhost:${info.port}`)
})
結果3
無事jsやcssをブラウザが取得でき、アプリが描画されました。
routingのあるSPAの場合
react-routerやvue-routerを利用している場合はすべてのパス(eg; /react-app/foo)に対してindex.htmlを返す必要があります。
ブラウザのURLが/react-app/fooの場合はリクエストが/react-app/foo.htmlになるためそのようなファイルは存在せず404エラーとなります。
そのため
if (path.extname(filePath) === '.html') {
filePath = '../dist/react-app/index.html';
}
のように読み込むパスを変更するなどの工夫が必要ですね