Skip to content

はじめに

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

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

モチベーション

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サイトの開発。拡張性よりもフロントエンドに特化したユーティリティが欲しいと思いました。

それなら自分で書く

と思い立ち、要件を洗い出します。

必須要件

追加要件(できれば叶えたい)

これらをなるべく満たせる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/

はもろに影響を受けました。
シンプルな記述で、どのアクションが想定されているのか誰が見てもわかるコードはとてもメンテナンス性に優れており、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