必要最低限の機能を詰め込んだHTMLアプリケーション構築のためのフレームワーク "MiuJS"を作った話
はじめに
とても久しぶりに記事を書きます。
読みづらい部分があればご指摘ください。
Node.js製のWebフレームワークを作った話
(2022年4月27日 追記)
1分で説明するMiuJS
MiuJSは、小さなwebサイトを開発するために必要なユーティリティを含んだNode.jsで動くフルスタックフレームワークです。
ReactやVue.jsなどの特定のフロントエンドライブラリに依存せず、それでいて開発に必要な機能をなるべくたくさん詰め込みました。
MiuJS特徴
- SSGではなくサーバーサイドで稼働
- GET以外のリクエストも処理できるコントローラーを内蔵
- Nunjucksテンプレートを使用したHTMLファーストの開発、
fs
を使用しないテンプレートの事前ビルド - CSSファイルを量産しない、Scoped CSS機能
- クライアント側のJavaScriptバンドルの外部ライブラリ依存無し(本番用ビルドでは初期状態で5kb以下)
- live reloadを備えた開発サーバーおよびconnectを使用した本番用ビルトインサーバー内蔵
MiuJSに向いていること
- サービスのLPなどのクライアント側に負荷をかけたくないウェブページ作成
- 特定のプラットフォームに依存しない軽量ウェブサイト
- テンプレート+少量のPOSTアクションを備えたコーポレートサイトなど
MiuJSに向いていないこと
- 大規模なウェブサイト及びウェブアプリケーション開発
- SPA開発
- ローカルに大量のMarkdownを含むブログやドキュメントサイト
以上の狭い需要ではありますが、ちょっとしたサイト構築をスピーディにこなしたいときに既存のフレームワークがオーバースペックに感じてしまっている方には便利に使ってもらえるのでは、と思っています。
興味のある方は是非読み進めてみてください。
モチベーション
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。一応上記の要件をすべて満たしています。
プロジェクト作成からビルドまで
詳細な利用はウェブサイトに記載しているため、簡易的なご紹介だけさせていただきます。
プロジェクト作成
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テンプレートを使用しています。fs
の利用を避けています。
また、独自の実装でスコープ付きCSSを実装しています。このあたりはVue.jsやSvelteあたりから影響を受けています。
フレームワーク
MiuJSはいくつかのフレームワークに影響を受けています。
Sinatra
- HTTPメソッド名の関数を定義する記法
-
json
のようなコアパッケージから利用出来るResponse返却用関数
はもろに影響を受けました。
シンプルな記述で、どのアクションが想定されているのか誰が見てもわかるコードはとてもメンテナンス性に優れており、Sinatraで作った小さなWebサービス(?)は運用がしやすかった印象が強く残っています。
Next.js
pages
下のファイルシステムルーティングは、MiuJSでも採用しています。
基本的には同じ仕様ですが、[...paths].js
のようなスプレッド記法まではまだサポートしていません。
Turbo
live-frame
はTurboから着想を得ています。
Remix
サーバーのプリミティブを作成してプラットフォーム毎にミドルウェア関数を分離する手法もRemix由来です。
Hydrogen
HydrogenはShopify専用フレームワークですが、キャッシュヘッダーの生成などは一部利用させていただいています。
今後の実装
このフレームワークは「サーバーサイドが必要だけど現存のフルスタックフレームワークほどオーバースペックにしたくない程度の小さなウェブサイト」を開発するというニッチな需要を満たすものです。
必要な機能を実装する上で、上記のフレームワークからRemixのセッションまわりなどはほとんどコピーで間に合わせの実装となっています。大きなアプリケーションを開発することを想定していないので、そもそも利用シーンが限られる気もしますが・・・。
このあたりはPHPライクにもう少しシンプルに使えるように書き換える予定です。
このニッチな需要にマッチする方がいらっしゃったら、よければ使ってみてください。
npx create-miu@latest
Discussion