Denoのフロントエンド開発の動向【2022年春】
半年程前に、以下のような記事を書きました。
Denoでのフロントエンド開発に関して、ここ半年程でまた大きな動きがあったため、改めてまとめていきたいと思います。
Aleph.js v1.0.0のアルファバージョンがリリース
Deno製のフレームワークであるAleph.jsのv1.0.0 アルファバージョンが公開されました。
現在、v1.0.0のリリースに向けて積極的に開発が進められています。
Aleph.jsやesm.shなどの作者であるJe Xia氏がDeno社に加わったこともあり、ここ半年で大幅に開発が進んでいる印象です。
また、Aleph.jsは元々はNext.jsに影響を受けたフレームワークという位置づけでしたが、ここ最近ではRemixなどのフレームワークの影響も徐々に受けているような印象を感じています。
ここでは、Aleph.js v1に向けて行われている大きな変更などについて解説していきます。
Deno Deployサポート
Deno DeployとはCDN Edge上でJavaScriptやTypeScriptなどを実行するための分散ホスティングサービスです。
現在、Aleph.jsではこのDeno Deployをサポートするための対応が進められています。
実際に公式で公開されているデモアプリもDeno Deployに移行されつつあります。
例)
この対応が進めば、将来的にはCDN Edge上でSSRの実行などができるようになりそうです。
deno.json
のサポート
deno.json
はDenoの設定ファイルで、これによってリンタ(deno lint
)やタスクランナ(deno task
)などの挙動をカスタマイズすることができます。
aleph init
コマンドでプロジェクトを作成した際に、このdeno.json
が自動で作成されるようになりました。
$ deno run -A https://deno.land/x/aleph@1.0.0-alpha.27/cli.ts init
生成されたdeno.json
には開発に必要な各種タスクがあらかじめ定義されており、今後はdeno task
コマンドによって様々な処理を行うことができます。
# devサーバの起動
$ deno task dev
# ビルド
$ deno task build
ディレクトリ構成の変更
後述するAPIエンドポイントに関する変更にも関連しますが、pages/
とapi/
ディレクトリが廃止され、routes/
ディレクトリに統合されています。
また、従来までに存在していたaleph.config.ts
という設定ファイルが廃止され、代わりにserver.tsx
というファイルが導入されました。
今後はこのserver.tsx
でAleph.jsの設定をカスタマイズする必要があります。
import { Router } from "aleph/react";
import { serve } from "aleph/server";
import { renderToReadableStream } from "react-dom/server";
serve({
config: {
// ルーティングに関する設定
routes: "./routes/**/*.{tsx,ts}",
},
// SSRに関する設定
ssr: {
suspense: true,
render: (ctx) => renderToReadableStream(<Router ssrContext={ctx} />, ctx),
},
});
その他にも、main.tsx
というファイルが追加されており、このファイルはクライアントのエントリポイントとして振る舞います。
APIエンドポイントの定義方法が変更
今まで、Aleph.jsではapi/
ディレクトリにハンドラを用意することでAPIエンドポイントを作成することができました。
このAPIエンドポイントの定義方法に大きな変更が行われました。
具体的には、下記のようにroutes/
配下のコンポーネントファイル内でdata
をexport
する必要があります。
import { useData } from "aleph/react";
import { useState } from "react";
import type { FormEvent } from "react";
interface User {
id: number;
name: string;
}
let nextUserId = 0;
const users: Array<User> = [
{ id: nextUserId++, name: "foo" },
{ id: nextUserId++, name: "bar" },
];
export const data: Data = {
get(_req, ctx) {
return ctx.json({ users });
},
async post(req, ctx) {
const { name } = await req.json();
const id = nextUserId++;
const user = { id, name };
users.push(user);
return ctx.json({ users });
},
};
export default function Users() {
const { data, isMutating, mutation } = useData<{ users: Array<User> }>();
const [userName, setUserName] = useState("");
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutation.post({ name: userName }, { replace: true }).then(() => {
setUserName("");
});
};
return (
<div>
<h1>Users</h1>
<ul>
{data.users.map((x) => {
return <li key={x.id}>{x.name}</li>;
})}
</ul>
<form
onSubmit={onSubmit}
>
<input
type="text"
name="name"
value={userName}
onChange={(e) => setUserName(e.target.value)}
disabled={Boolean(isMutating)}
/>
</form>
</div>
);
}
このようにページコンポーネントとAPIを同一ファイル内で定義します。
定義したエンドポイントとやり取りするにはuseData()
を使います。
const { data, isMutating, mutation } = useData<{ users: Array<User> }>();
useData()
によって返却されるdata
には、get
エンドポイントから取得されたデータが自動で設定されます。
mutation.post({ name: userName }, { replace: true });
また、post
やdelete
などのエンドポイントとやり取りする際は、上記コードのようにuseData()
から返却されるmutation
オブジェクトを利用します。
React以外のフレームワークのサポート
Aleph.jsにはloaderという機能があり、これによって独自のファイルタイプをサポートすることができます。
現在、Aleph.jsにはVue.js向けのloaderが存在し、これによりSFCの読み込みがサポートされています。
ただし、Vue.js向けにはルーティングなどの機能がまだサポートされていないようです。現在のVue.jsサポートに関する状況については、下記ロードマップを参照ください。
UnoCSSの組み込みサポート
Aleph.jsにUnoCSSの組み込みサポートが加わっています。
UnoCSSを有効化する際は、server.tsx
で例えば、以下のように設定します。
import presetUno from "@unocss/preset-uno.ts";
import { Router } from "aleph/react";
import { serve } from "aleph/server";
import { renderToReadableStream } from "react-dom/server";
serve({
config: {
routes: "./routes/**/*.{tsx,ts}",
// UnoCSSの設定
unocss: {
presets: [presetUno()]
}
},
ssr: {
suspense: true,
render: (ctx) => renderToReadableStream(<Router ssrContext={ctx} />, ctx),
},
});
これにより@unocss/preset-unoが利用できるようになります
また、必要に応じてリセットCSSも読み込ませるとよいでしょう。
<link rel="stylesheet" href="https://esm.sh/@unocss/reset@0.32.1/tailwind.css">
その他の変更点
-
jsx
/tsx
/ts
などのオンデマンドでの変換- Viteなどと同様、必要に応じてコードが変換される
- React v18サポート
- ビルド時のesbuildによるバンドルがサポート
Fresh
FreshはDeno Deployで動作させることを想定したPreactベースのフレームワークです。
事前のビルドステップを必要とせず、CDNエッジ(Deno Deploy)上でオンデマンドでのビルド及びレンダリングを提供するのが大きな特徴です。
このFreshについてもここ半年程で大きな動きがありました。
Island Architectureのサポート
FreshでIsland Architecture[1]がサポートされました。
この変更に合わせて、ディレクトリ構造にも大きな見直しが行われています。
-
pages/
ディレクトリがroutes/
にリネーム-
routes/
にはコンポーネントまたはAPIエンドポイントを配置します。 routes/
ディレクトリのコンポーネントは従来通りSSRされますが、Hydrationは行われません。
-
-
islands/
ディレクトリが追加- このディレクトリに配置したコンポーネントはクライアント上でレンダリング及びHydrationされます。
-
routes.gen.ts
がfresh.gen.ts
へリネーム-
fresh.gen.ts
はfresh manifest
コマンドで自動生成または更新されます。
-
これらの変更に合わせて、useData()
フックや<Suspense>
コンポーネントなどが一時的に削除されています(これらは近い将来に再度実装される予定のようです)
Custom App及びCustom Error Pageのサポート
routes/_app.tsx
を用意することで、カスタムのAppコンポーネントを定義できるようになりました。
/** @jsx h */
/** @jsxFrag Fragment */
import { AppProps, h, Fragment, Head } from "../client_deps.ts";
export default function App(props: AppProps) {
return (
<>
<Head>
<meta name="description" content="Sample Fresh App" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Sample Fresh App" />
</Head>
<props.Component />
</>
);
}
また、カスタムのエラーページもサポートされています。
例えば、routes/_404.tsx
を用意すると、404エラー発生時にレンダリングされるページを定義することができます。
/** @jsx h */
import { h, UnknownPageProps } from "../client_deps.ts";
export default function NotFound({ url }: UnknownPageProps) {
return (
<div>
<p>
Not Found: {url.pathname}
</p>
</div>
);
}
注意点として、これらのファイルを追加した際はfresh manifest
の再実行が必要です。
deno.landがFreshで再実装された
新機能とはちょっと関係ない話なのですが、Denoの公式サイトであるdeno.landがFreshで再実装されました。
元々、deno.landはNext.js+Vercelで動作していましたが、これにより全面的にDenoに書き換えられた状況です。
また、CSSについては元々TailwindCSSが使われていましたが、Denoでも動作するTwindへ移行されています。
安定版のリリース予定について
Freshの安定版リリースは、今のところ2022年5月末付近が予定されているようです。
安定版がリリースされるまではまだ大きな変更が行われる可能性もあるため、本番での利用などは基本的にまだ推奨されていない状況です。
Ultra v1がリリース
Deno製のReactベースのフレームワークであるUltraのv1がリリースされました。
APIルートのサポート
UltraでAPIルートがサポートされました。
src/api
ディレクトリ配下でRequest
を受け取りResponse
またはPromise<Response>
を返却する関数をdefault export
することで、APIエンドポイントを定義することができます。
例えば、src/api/hello.ts
に下記のようなファイルを用意しておくと、/api/hello
からAPIにアクセスできます。
export default () => {
return new Response("Hello");
};
deno.json
との統合
Ultraの利用時にdeno.json
が要求されるようになりました。
今後はサーバの起動などもdeno task
で行う想定のようです。
# 例) devサーバを起動する
$ deno task dev
また、Ultraはルートディレクトリ配下のimportMap.json
に特定の依存関係が定義されていることを前提としています。
そのため、Ultraを利用する際はcreate-ultra-appをベースにプロジェクトを起こすと安全だと思います。
サードパーティモジュールのベンダリング
Deno本体のベンダリングの仕組み(deno vendor
)とは異なる独自のベンダリングの仕組みが追加されました。(おそらく、UltraがImport Mapsファイルの使用を前提としており、deno vendor
とは噛み合わなかったためだと思われます)
deno task vendor
コマンドを実行すると、プロジェクトの依存関係を.ultra/vendor
に書き込むことができます。
$ deno task vendor
ベンダリングされた依存関係は、deno task vendor
によって生成されたvendorMap.json
をdeno.json
のimportMap
オプションに設定することで読み込むことができます。
その他の変更点
RemixがDenoを実験的にサポート
Remix v1.2.0でDenoのアダプタ[2]が実装されました。
これにより、Remixアプリのデプロイ先としてDeno Deployなどを利用する余地が生まれました。
プロジェクトの初期化
create-remix
での初期化時にdenoテンプレートを使うと、Deno向けのプロジェクトを生成できます。
$ yarn create remix --template https://github.com/remix-run/remix/tree/v1.4.3/templates/deno
これにより、プロジェクトの雛形が生成されます。
devサーバの起動
下記コマンドでdevサーバを起動できます。
$ yarn dev
このコマンドを実行すると、下記の処理が実行されます。
-
remix build
によるサーバのビルド (build/index.js
) -
remix watch
によるファイルの変更監視及び再ビルドの有効化 -
build/index.js
の起動 (Denoで実行されます)
本番ビルド
本番環境向けのビルドを実行する際は、下記で行えます。
$ yarn build
ディレクトリ構成について
deno
テンプレートを使用してプロジェクトを生成した場合、通常のRemixテンプレートを使用したときと比べて、いくつか異なる点があります。
ここでは特筆すべき点について簡単に解説します。
remix-deno
ディレクトリ
このディレクトリにはDenoアダプタの実体が含まれます。
このディレクトリ内のファイルは直接編集すべきではありません。
server.ts
サーバのエントリポイントです。
ビルド後のサーバ(build/index.js
)は、このファイルの内容を元に生成されています。
app/deps
ディレクトリ
Reactなどの依存パッケージはこのディレクトリで管理されています。
各依存パッケージはesm.shからimport
されていますが、これらの依存関係はesbuild-plugin-cacheを使用してビルド時に解決しているようです。
制限
remix
CLI(@remix-run/dev
)を使う必要がある関係上、開発時にはDenoに加えてNode.jsも必要になります。
また、ファイル変更時のライブリロードはまだサポートされていません。ファイルの変更時は手動でブラウザをリロードする必要があります。
AstroがDenoアダプタをサポート
AstroはNode.js製の静的サイトジェネレータです。
Island Architecture[1:1]を採用していたり、React, Vue.js, 及びSvelteなど様々なフレームワークをサポートしているところなどが大きな特徴です。
このAstroのv0.26.0でDenoアダプタがサポートされました。
これにより、SSRの実行環境としてDenoを利用できるようになりました。
利用方法
Denoアダプタを利用するには、まず@astrojs/denoパッケージを導入する必要があります。
このパッケージはastro add
コマンドで追加することができます。
$ astro add deno
これにより、Denoアダプタのインストールと有効化を実施できます。
Denoアダプタを有効化した状態でビルドを実行すると、dist/server/entry.mjs
というファイルが生成されます。
$ astro build
このdist/server/entry.mjs
がサーバのエントリポイントになります。
main.js
を用意し、このファイルをimportしてみましょう。
import "./dist/server/entry.mjs";
main.js
をDenoで実行すると、サーバが起動します。
$ deno run --allow-read --allow-net main.js
そして、サーバにアクセスするとDenoによってAstroコンポーネントがSSRされます。
このようにして、Astro製のアプリをDenoで動作させることができます。
SvelteKitのDenoアダプタが公開
非公式ですがSvelteKitのDeno向けのアダプタが公開されています。
これを利用することで、SvelteKit製のアプリをDeno Deployなどにデプロイすることができます。
例) https://svelte-adapter-deno.deno.dev/
ちなみに、DenoでSvelteアプリを開発するためのツールとしてSnelというものも存在し、こちらを使ってDenoでSvelteアプリを開発することもできます。
FaaSの拡充
Supabase Functions
Deno社とSupabaseにより、共同でSupabase Functionsが公開されました。
Deno Deployをベースにしており、ローカルで記述したTypeScriptの関数をそのままSupabase Functionsへデプロイすることができます。
また、supabase-jsを使用してデータベースなどを操作することもできるようです。
実際のSupabase Functionsの利用方法などについては下記記事などで詳しく解説されています。
Netlify Edge Functions
Deno社とNetlifyにより、共同でNetlify Edge Functionsのpublic betaバージョンが公開されました。
これを利用することで、Netlify上のサイトに様々な機能を追加することができます。
こちらもSupabase Functionsと同様にDeno Deployをベースにしており、TypeScriptによって関数を記述することができます。
また、現在、様々なフレームワークでNetlify Edge Functionsのサポートが進められています。
タスクランナ
この記事内でもすでに何度か登場していますが、Deno本体にタスクランナ(deno task
コマンド)が実装されました。
deno task
を利用するには、まずdeno.json(c)でタスクを定義する必要があります。
{
"tasks": {
"test": "deno test --allow-read=tests/tmp --allow-write=tests/tmp ./tests"
}
}
定義したタスクは次のようにして実行することができます。
# `deno test --allow-read=tests/tmp --allow-write=tests/tmp ./tests`が実行されます
$ deno task test
このdeno task
の大きな特徴として、標準でcross-env相当の機能やcd
やmv
などのコマンドを組み込みで提供していることが挙げられます。
そのため、WindowsやMacなどOSを問わずに統一された方法で定義したタスクを実行できるのがメリットです。
テスト
Denoの標準モジュールであるdeno_stdでテスト関連の機能の拡充が行われています。
deno_std/testing/bdd
JestやMochaライクなスタイルでテストコードを記述することができます。
import { assertEquals } from "https://deno.land/std@0.135.0/testing/asserts.ts";
import { describe, it } from "https://deno.land/std@0.135.0/testing/bdd.ts";
describe("sum", () => {
it("should return sum of numbers", () => {
assertEquals(sum(1, 2, 5), 8)
});
it("should return 0 when no arguments are given", () => {
assertEquals(sum(), 0);
});
});
定義したテストコードは通常通りdeno test
コマンドで実行できます。
deno_std/testing/mock
deno_std/testing/mock
モジュールが追加されました。
このモジュールで提供されているspy()
を使用することで、関数の利用を追跡することができます。
import { assertSpyCall, assertSpyCalls, spy } from "https://deno.land/std@0.133.0//testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.133.0/testing/asserts.ts";
function add(a: number, b: number): number {
return a + b;
}
const addSpy = spy(add);
assertEquals(addSpy(1, 2), 3); // => OK
assertEquals(addSpy(2, 3), 5); // => OK
assertSpyCall(addSpy, 0, {
args: [1, 2],
returned: 3,
}); // => OK
assertSpyCall(addSpy, 1, {
args: [2, 3],
returned: 5,
}); // => OK
assertSpyCalls(addSpy, 2); // => OK
また、stub()
を使用することで、特定のオブジェクトのメソッドをスタブすることができます。
import { assertSpyCalls, returnsNext, stub } from "https://deno.land/std@0.133.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.133.0/testing/asserts.ts";
const randomUUID = stub(crypto, "randomUUID", returnsNext([
"00000000-0000-0000-0000-000000000000",
"11111111-1111-1111-1111-111111111111",
]));
try {
assertEquals(crypto.randomUUID(), "00000000-0000-0000-0000-000000000000"); // => OK
assertEquals(crypto.randomUUID(), "11111111-1111-1111-1111-111111111111"); // => OK
assertSpyCalls(randomUUID, 2); // => OK
} finally {
randomUUID.restore();
}
deno_std/testing/snapshot
)
スナップショットテスティング(スナップショットテスティング用にdeno_std/testing/snapshot
モジュールが追加されています。
import { assertSnapshot } from "https://deno.land/std@0.136.0/testing/snapshot.ts"
Deno.test("doSomething", async (t) => {
const result = doSomething();
await assertSnapshot(t, result);
});
assertSnapshot
を利用することで、__snapshots__
ディレクトリに保存されたスナップショットと現在の状態を比較することができます。(スナップショットを読み込む必要があるため、利用するには--allow-read
の指定が必要です)
また、スナップショットを更新したいときは、テストを実行する際に--update
を指定する必要があります。
# スナップショットは__snapshots__に書き込まれます
$ deno test --allow-read --allow-write tests/some_test.js -- --update
この場合、スナップショットを書き込むために、追加で--allow-write
が必要になります。
まとめ
Deno製の各種フレームワークで安定化に向けた動きが見られました。
現在、Aleph.jsやFreshで安定版リリースに向けた動きが行われており、これらの安定版がリリースされれば、フロントエンド開発での採用も徐々に増えていくのではないかと想像しています。
また、RemixやAstroなどのNode.jsベースのフレームワークでも少しずつDenoのサポートが進んでいます。
これにより、これらフレームワークで開発したアプリのデプロイ先としてDeno Deployなどを利用するという選択肢も生まれてきそうです。
ただし、いずれもDeno単独での開発はまだ行えず、Node.jsを併用する必要があります。
Denoではこういった問題などを解消するために、Node.js互換モードの開発が進められています。
このNode.js互換モードが発展していけば、例えば、Deno+Remixでアプリケーションを開発し、デプロイ先としてDeno Deployなどを利用するという選択肢も将来的には生まれてくるかもしれません。
参考
- https://github.com/alephjs/aleph.js/releases/tag/1.0.0-alpha.1
- https://github.com/alephjs/aleph.js/releases/tag/1.0.0-alpha.20
- https://github.com/exhibitionist-digital/ultra/releases/tag/v1.0.0
- https://deno.com/blog/supabase-functions-on-deno-deploy
- https://supabase.com/docs/guides/functions
- https://deno.com/blog/netlify-edge-functions-on-deno-deploy
- https://www.netlify.com/blog/announcing-serverless-compute-with-edge-functions/
-
主にSSRの文脈において、ページ全体を細かなチャンク(islands)に分割し、それらのチャンクを独立してHydrationすることによりパフォーマンスの向上などが期待できるというものです。より詳しくはIslands Architectureを参照ください。 ↩︎ ↩︎
-
アダプタとはプラットフォームに応じて差異を吸収することで、様々なプラットフォームでRemixアプリを動作させるための仕組みです。 ↩︎
Discussion