Web開発
Primate は最後の偉大なWebフレームワークである
Primate Is the Last Great Web Framework (superarch.org)
要約
Primateの作者は、偉大なWebフレームワークはルーティング、レンダリング、データ管理などスタック全体を包括すべきだと主張しています。JavaScriptエコシステムはコンポーネントの組み合わせを重視する傾向がありますが、Primateはフロントエンド、バックエンド言語、ランタイムに依存せず、開発者がルートごとに最適な技術を選択できる柔軟性を提供します。これにより、ベンダーロックインや不必要な意思決定から開発者を解放し、統一されたアプリケーションモデルを提供することを目指しています。
全文翻訳
Primateの作者なので、タイトルには適切な注意を払ってください。
「偉大なWebフレームワーク」とは、スタック全体を所有するツールを指します。ルーティング、レンダリング、データ、バリデーション、セッション、デプロイターゲット、ランタイムサポートなどです。これらのピースは互いに適合するように設計されるべきです。
私はPHPフレームワークでWebアプリケーションを構築して育ちました。主にYii、一部Laravelです。それらのPHP自体ではなく、フレームワークがスタックを所有しているという感覚が気に入っていました。データベース、テンプレート、ルーティング、バリデーション、MVCのようなパターンです。それらは、一つのまとまったシステムの中でピースを交換することができました。フレームワークのウェブサイトに行けば、公式にサポートされているものがわかります。何が馴染みのある領域で、何が地図の外かを理解していました。
JavaScriptがサーバーサイドに移るにつれて、その考え方はほとんど消滅しました。JavaScriptエコシステムは、凝集性よりも構成を強く好みます。サーバーフレームワークを選び、次にデータベースクライアント、次にバリデーション、次にセッション、次にフロントエンド、次にビルドツールを選び、それらをすべてつなぎ合わせます。各ピースは単独では優れているかもしれませんが、誰もそのつなぎ目を所有しません。
UNIXの純粋主義者は、それが正しいモデルだと言うかもしれません。各ツールは一つのことをうまくやるべきです。しかし、Webアプリケーションは単なる独立したツールのパイプラインではありません。それらは、リクエストの形状、バリデーションの境界、セッション処理、レンダリング、ルーティング、シリアライゼーション、デプロイターゲットなど、多くの共有された仮定に満ちています。実際には、それらのツールが出会うところで、物事はしばしば微妙な方法で失敗します。
メタフレームワークはこの状況を改善しましたが、それは世界を締め付けることによってのみでした。Next.jsはまとまったスタックを提供しますが、それはReactスタックです。Solidを発見して試したいと思った場合、ビューレイヤーを単純に切り替えるだけではありません。異なる規約、異なるルーティングの詳細、異なるバックエンドの仮定を持つ別のメタフレームワークに移行します。エコシステム全体で同じパターンが繰り返されます。ファイルシステムルートに加えて、一つの祝福されたフロントエンドが、何度も再構築されます。Reactには一つのメタフレームワークが与えられます。Solidには別のものが与えられます。Svelteには別のものが与えられます。Vueには別のものが与えられます。それぞれがほとんど同じバックエンドの問題を解決しますが、毎回再学習する必要のあるわずかに異なる方法で解決します。
ランタイムの断片化はこれを悪化させました。Node、Deno、Bunはすべて能力のあるJavaScriptランタイムですが、一部のフレームワークは事実上ランタイム固有になっています。つまり、あなたのアプリはフロントエンドだけでなく、ランタイムにも縛られる可能性があります。そして、フレームワークがDenoやBunを「サポート」すると言うとき、それはしばしばランタイムがNodeコードを実行するのに十分互換性があるという意味にすぎません。それは必ずしもフレームワークが各ランタイム独自のAPIと実行モデルを使用するように設計されたことを意味しません。
これに理由はありません。フロントエンドはブラウザUIを記述する方法です。バックエンドはリクエスト、データ、アプリケーションロジックを処理する方法です。ランタイムは実行ターゲットです。これらのものは独立して変化させられるべきです。
あなたはReactで一つのルートを書き、Svelteで別のルートを書き、Markoで別のルートを書き、同じバックエンドモデルを維持できるべきです。
あなたはTypeScriptでバックエンドルートを書き、Go、Ruby、またはPythonで書き、それらを一つのアプリ内で構成できるべきです。
あなたはコードを一行も変更せず、ランタイム固有のパッケージを使用してギャップを埋めることなく、Node、Deno、またはBunで同じアプリを実行できるべきです。そして、フレームワークは依然としてバリデーション、データベースストア、セッション、i18n、ルーティング、レンダリングの公式サポートを提供すべきです。
それがPrimateが構築された目的です。
Primateは、一つのフロントエンド、一つのバックエンド言語、または一つのランタイムに属さないWebフレームワークです。それは一つのアプリケーションモデルを提供し、ルートごとにピースを選択できるようにします。
Reactが意味をなす場所ではReactを使用してください。Svelteが良いと感じる場所ではSvelteを使用してください。ほとんどのルートではTypeScriptを使用してくださいが、そのエコシステムやランタイム特性がより適している場所ではGo、Ruby、またはPythonを使用してください。今日Nodeで実行し、明日Denoで、後にBunで実行してください。
Primateは誇大広告ではありません。ベンチマークシアターではありません。ポイントは新しさではありません。ポイントは、結合、ロックイン、そして他の人々があなたのために行う決定を取り除くことです。必要であれば公式スタックの外に手を伸ばすことができますが、中に留まる場合、そのつなぎ目はPrimateの責任です。
Webフレームワークは、あなたの選択を所有することなく、スタックを所有すべきです。
小さな例
ここでは、すでにそのポイントを示している小さなアプリがあります。一つのバックエンドモデル、複数のフロントエンド、フレームワークの切り替えなし。
プロジェクトを作成します:
Copymkdir primate-demo
cd primate-demo
npm init -y --init-type=module
npm install primate @primate/react @primate/svelte react react-dom svelte
フロントエンドを設定します:
Copy// config/app.ts
import react from "@primate/react";
import svelte from "@primate/svelte";
import config from "primate/config";
export default config({
modules: [
react(),
svelte(),
],
});
Reactルートとコロケーションされたページを作成します:
Copy// routes/index.ts
import response from "primate/response";
import route from "primate/route";
export default route({
get() {
return response.page({
title: "One app",
links: [
{ href: "/react", label: "React route" },
{ href: "/svelte", label: "Svelte route" },
],
});
},
});
Copy// routes/index.tsx
import route from "./index";
export default function Index({ title, links }: typeof route.get.Page) {
return (
<main>
<h1>{title}</h1>
<p>Same app. Same routing model. Different frontends per route.</p>
<ul>
{links.map(link => (
<li>
<a href={link.href}>{link.label}</a>
</li>
))}
</ul>
</main>
);
}
別のReactルートとページを追加します:
Copy// routes/react.ts
import response from "primate/response";
import route from "primate/route";
export default route({
get(request) {
return response.page({
from: request.query.try("from"),
});
},
});
Copy// routes/react.tsx
import route from "./react";
export default function React({ from }: typeof route.get.Page) {
return (
<main>
{from && <h1>Hello from {from}</h1>}
<p>This route is rendered with React.</p>
<a href="/svelte?from=react">Go to the Svelte route</a>
</main>
);
}
同じアプリにSvelteルートを追加します:
Copy// routes/svelte.ts
import response from "primate/response";
import route from "primate/route";
export default route({
get(request) {
return response.page({
from: request.query.try("from"),
now: new Date().toISOString(),
});
},
});
Copy<!-- routes/svelte.svelte -->
<script lang="ts">
import route from "./svelte";
const props: typeof route.get.Page = $props();
</script>
<main>
{#if props.from}<h1>Hello from {props.from}</h1>{/if}
<p>This route is rendered with Svelte.</p>
<p>Server time: <code>{props.now}</code></p>
<a href="/react?from=svelte">Go to the React route</a>
</main>
実行します:
Copynpx primate
または、Denoを好む場合は deno run -A npm:primate を使用し、Bunの場合は bunx --bun primate を使用します。
重要なのはここです。バックエンドルートモデルは変更されませんでした。アプリはReactアプリやSvelteアプリにはなりませんでした。Primateアプリのままで、各ルートが意味のあるフロントエンドを選択しました。
* このような制限は、Primateでも存在します。異なるフロントエンドフレームワークは、現在レイアウトを共有できません。なぜなら、レイアウト自体は一つのフロントエンドによってレンダリングされるからです。これは将来的にWebコンポーネントで可能になるかもしれません。