はじめに

とても久しぶりに記事を書きます。
読みづらい部分があればご指摘ください。

Node.js製のWebフレームワークを作った話

https://github.com/tknf/miujs

(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を作る、などとなるとこれらのフレームワークはオーバーエンジニアリングに感じます。
選択肢はたくさんありますが、主に感じていた問題は以下。

  1. Next.jsやRemixなどで使用されるReactは好きだが、小さなLP程度のサイトを制作するのにはバンドルサイズが気になる
  2. Svelte製のSvelteKitも1.と同様
  3. WordPressなどのCMSも同様にオーバースペック気味&そもそもファイル群が多すぎて見通しが悪い
  4. 静的サイトジェネレータ(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のサーバーリクエストは以下の順で処理されます。

  1. createVercelRequestHandlerなどのプラットフォーム毎に作成されたリクエストハンドラ
  2. src/routes下のファイルに記述したget postなどのリクエストメソッドに対応する関数の呼び出し
  3. src/entry-server.jscreateServerRequest関数

基本的には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/partialssrc/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://sinatrarb.com/

  • 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