はじめに
とても久しぶりに記事を書きます。
読みづらい部分があればご指摘ください。
Node.js製のWebフレームワークを作った話
モチベーション
2022年現在、「Webフレームワーク」と名前の付くライブラリやmodは言語を問わず数えきれないほど存在しています。
実際に製品のコアとなるソフトウェアを開発する際は、Railsなどの大きなフレームワークを使用することもあります。
しかし、その製品を紹介するためのLPを作る、などとなるとこれらのフレームワークはオーバーエンジニアリングに感じます。
選択肢はたくさんありますが、主に感じていた問題は以下。
- Next.jsやRemixなどで使用されるReactは好きだが、小さなLP程度のサイトを制作するのにはバンドルサイズが気になる
- Svelte製のSvelteKitも1.と同様
- WordPressなどのCMSも同様にオーバースペック気味&そもそもファイル群が多すぎて見通しが悪い
- 静的サイトジェネレータ(Hugoなど)を使用する選択肢もあるが、データ更新の度にビルドが必要
とにかく、
- 開発に時間をかけなくて良く
- サイズが小さく
- サーバーサイドで稼働する
Webフレームワークがあれば、と感じていました。
現存する選択肢を考える
まずは、上記を満たせるフレームワークの選択肢を絞ってみました。
SinatraやGinなどの小さなフレームワークに絞って色々と試してみますが、傾向としてHTTPルーターを拡張した程度のカスタマイズ前提のものが多いように感じました。
しかし今回の目的は飽くまでLPなどの小さなWebサイトの開発。拡張性よりもフロントエンドに特化したユーティリティが欲しいと思いました。
それなら自分で書く
と思い立ち、要件を洗い出します。
必須要件
- 学習コストの低いテンプレートエンジンが使用できること
- サーバーサイドレンダリングのサポート
- 静的サイトジェネレータでないこと
- HTTPサーバー内蔵、POSTリクエストも処理できる
追加要件(できれば叶えたい)
- JavaScriptバンドル(キャッシュ対策)
- スコープ付きCSS、またはCSSモジュールなどが使用できる(クラス名を考えたくない)
- JavaScript無しでも動かせる(サーバーサイドのみで完結できる)
- 開発時のライブリロード(ブラウザの更新ボタン押したくない)
- サーバーのランタイムに
fs
を含まない(Vercel Serverless functionsやNetlify functionsなどで動かしたい)
これらをなるべく満たせるWebフレームワークの開発をしました。
MiuJS
そして出来たのがMiuJS。一応上記の要件をすべて満たしています。
https://www.miujs.com
プロジェクト作成からビルドまで
詳細な利用はウェブサイトに記載しているため、簡易的なご紹介だけさせていただきます。
プロジェクト作成
create-miu
パッケージにより、npxから作成可能です。
npx create-miu@latest my-project
現段階では、デプロイターゲットはビルトインサーバー、Netlify、Vercelから選択可能で、それぞれJavaScriptとTypeScript用のテンプレートを用意しています。
開発サーバー
ライブリロードを備えた開発サーバーを内蔵しています。
yarn dev
リクエストフロー
MiuJSのサーバーリクエストは以下の順で処理されます。
createVercelRequestHandler
などのプラットフォーム毎に作成されたリクエストハンドラsrc/routes
下のファイルに記述したget
post
などのリクエストメソッドに対応する関数の呼び出しsrc/entry-server.js
のcreateServerRequest
関数
基本的にはMVCでいうところのコントローラの役割を各Routeファイルが担っていて、詳細な処理はここに記述出来ます。
Routeファイル
src/routes
下ではNext.jsのようなファイルシステムルーティングを採用しており、src/routes/index.js
は/
、src/routes/about.js
は/about
といった具合に自動的にルーティングされます。
また、各RouteファイルはHTTPメソッド名の関数をexportすることで実装できます。
import type { RouteAction } from "miujs/node";
import { render, json } from "miujs/node";
// http://localhost:3000/posts#GET
export const get: RouteAction = ({ createContent }) => {
return render(createContent({ layout: "default" }), { status: 200 });
};
// http://localhost:3000/posts#POST
export const post: RouteAction = ({ qeury, params }) => {
console.log(`query: `, qeury);
console.log(`params: `, params);
return json({}, { status: 200 });
};
テンプレート
RouteAction
から渡されるcreateContent
関数は、ビルド後にキャッシュされたのNunjucksテンプレートからfs
を使用せずにテンプレートファイルを利用するための機構が組み込まれており、この関数を使用することで規定のディレクトリからNunjucksをレンダリングしたhtmlを生成出来ます。
import type { RouteAction } from "miujs/node";
import { render } from "miujs/node";
export const get: RouteAction = async ({ createContent, params }) => {
const data = await fetchSource({ handle: params!.handle }).catch(() => null);
if (!data) {
return render(createContent({ layout: "404" }), { status: 404 });
}
return render(
createContent({
layout: "default", // src/layouts以下のファイルを参照するエントリーポイントとなるテンプレート
sections: [ // src/sections以下のファイルを参照するセクション名とスコープ変数
{ name: "header", settings: { name: "Akiyoshi" } }
],
data // グローバルに注入するデータ
}),
{ status: 200, headers: { "Cache-Control": "public, max-age=900" } }
);
};
<!DOCTYPE html>
<html>
<head>
`data`はグローバルに参照できます。
<title>{{ data.title }}</title>
</head>
<body>
以下のコメントフラグメントに`sections`の内容がコンパイル&挿入されます
<!-- content -->
</body>
</html>
<header>
`settings`スコープから、セクションごとのスコープ変数にアクセス出来ます。
I'm {{ settings.name }}
</header>
スコープ付きCSS
Vue.jsやSvelteのようなマークアップでsrc/partials
とsrc/sections
内のコンテンツにスコープ付きCSSを適用することが出来ます。
<style scoped>
.price:scope {
display: flex;
align-items: center;
}
</style>
<template>
<div class="price"><small>$</small>{{ price }}</div>
</template>
ビルド
ビルドについても、コマンド一つで完了します。
yarn build
miu.config.js
に記述した設定に基づいて、それぞれのサーバーターゲット(node, netlify, vercel)向けにビルドします。
デプロイ
ビルトインサーバーであればNode.jsのみで動作するため、Node.jsランタイムが使用できるすべての環境にデプロイ可能です。
Google App EngineやHerokuなど、お好きなPaasを使用してください。
yarn serve
VercelやNetlifyなどのサーバーレス関数を使用したサービスへのデプロイは設定に少しコツが必要ですが、create-miu
パッケージのテンプレートに設定ファイルも含まれているので、特殊な処理をしなければならないケースを除けば、設定無しでデプロイ可能です。
技術的な話
テンプレートエンジン
MiuJSではNunjucksテンプレートを使用しています。
https://mozilla.github.io/nunjucks/
外部テンプレートの読み込みはビルド時にキャッシュしたものをロードすることで、fs
の利用を避けています。
また、独自の実装でスコープ付きCSSを実装しています。このあたりはVue.jsやSvelteあたりから影響を受けています。
フレームワーク
MiuJSはいくつかのフレームワークに影響を受けています。
Sinatra
- HTTPメソッド名の関数を定義する記法
json
のようなコアパッケージから利用出来るResponse返却用関数
はもろに影響を受けました。
シンプルな記述で、どのアクションが想定されているのか誰が見てもわかるコードはとてもメンテナンス性に優れており、Sinatraで作った小さなWebサービス(?)は運用がしやすかった印象が強く残っています。
Next.js
https://nextjs.org
Next.jsで使いやすかったpages
下のファイルシステムルーティングは、MiuJSでも採用しています。
基本的には同じ仕様ですが、[...paths].js
のようなスプレッド記法まではまだサポートしていません。
Turbo
https://turbo.hotwired.dev/
フロントエンドで別ページのコンテンツを非同期で読み込むユーティリティ用のwebコンポーネントlive-frame
はTurboから着想を得ています。
Remix
https://remix.run
セッションストレージやesbuildを使用したビルドの仕組みは大きく参考にさせていただきました。
サーバーのプリミティブを作成してプラットフォーム毎にミドルウェア関数を分離する手法もRemix由来です。
Hydrogen
https://hydrogen.shopify.dev
HydrogenはShopify専用フレームワークですが、キャッシュヘッダーの生成などは一部利用させていただいています。
今後の実装
このフレームワークは「サーバーサイドが必要だけど現存のフルスタックフレームワークほどオーバースペックにしたくない程度の小さなウェブサイト」を開発するというニッチな需要を満たすものです。
必要な機能を実装する上で、上記のフレームワークからRemixのセッションまわりなどはほとんどコピーで間に合わせの実装となっています。大きなアプリケーションを開発することを想定していないので、そもそも利用シーンが限られる気もしますが・・・。
このあたりはPHPライクにもう少しシンプルに使えるように書き換える予定です。
このニッチな需要にマッチする方がいらっしゃったら、よければ使ってみてください。
npx create-miu@latest