🦕

Denoのフロントエンド開発の動向【2022年秋】

2022/11/04に公開

半年程前に以下のような記事を書きました。

https://zenn.dev/uki00a/articles/frontend-development-in-deno-2022-spring

この半年の間に、Deno本体でnpmパッケージサポートが入るなどいくつか大きな動きがあったため、この記事ではそれらの動向について紹介いたします。

Deno本体でnpmパッケージの読み込みがサポート

Deno v1.25でDeno本体にnpmパッケージのサポートが入りました。

以下のように、npm:<パッケージ名>[@<バージョン>]形式のURLを指定することで、Denoからnpmパッケージを直接importすることができます。

import chalk from "npm:chalk@5.1.2";

chalk.yellow("foobar");

deno.land/xなどで公開されているパッケージと同様に、npm:で指定されたnpmパッケージについては、deno runなどのコマンドを実行する際に、npmレジストリから自動的にダウンロードされ、グローバルキャッシュ(DENO_DIR)に保存されます。

TypeScriptサポートについて

TypeScriptの型定義が同梱されたnpmパッケージについては、deno checkなどのコマンドを実行した際に、自動で型チェックを行ってくれます。

もし型定義ファイルが含まれないパッケージに対しても型チェックを適用したい際は、以下のように@deno-typesディレクティブで型定義を指定する必要があります。

// @deno-types="npm:@types/koa@2.13.5"
import Koa from "npm:koa@2.13.4";

const app = new Koa();

app.use(async ctx => {
  ctx.body = "Hello Deno!";
});

app.listen(3000);

Viteを動かす

Deno本体にnpmパッケージのサポートが入ったことにより、DenoでViteが動かせるようになりました。

https://www.youtube.com/watch?v=Zjojo9wdvmY

create-vite-extraというパッケージではDenoでViteを使用して開発するためのテンプレートが提供されています。

https://github.com/bluwy/create-vite-extra

以下のコマンドを実行すると、プロジェクトを生成することができます。

$ deno run --unstable --allow-env --allow-read --allow-write npm:create-vite-extra@latest

以下はdeno-vueテンプレートを使用した際に生成されるvite.config.mjsの例です。

vite.config.mjs
import { defineConfig } from 'npm:vite'
import vue from 'npm:@vitejs/plugin-vue'

import 'npm:vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
})

現時点では、DenoはまだpeerDependenciesをサポートしていないため、ワークアラウンドとしてvueパッケージをimportしておく必要があります。

また、deno.jsonも生成されます。

deno.json
{
  "tasks": {
    "dev": "deno run -A --unstable --node-modules-dir npm:vite",
    "build": "deno run -A --unstable --node-modules-dir npm:vite build",
    "preview": "deno run -A --unstable --node-modules-dir npm:vite preview",
    "serve": "deno run --allow-net --allow-read https://deno.land/std@0.157.0/http/file_server.ts dist/"
  }
}

このファイルでは、deno taskコマンドを使ってViteを動かすための様々なタスクが定義されています。

例えば、以下のコマンドを実行すると、Viteを使用してdevサーバが起動されます。

$ deno task dev

補足: --node-modules-dirについて

先程のdeno.jsonにおけるdevタスクの定義では、--node-modules-dirというオプションが使われていました。

$ deno run -A --unstable --node-modules-dir npm:vite

--node-modules-dirは、Viteなどのnode_modulesディレクトリの存在を前提としたパッケージをDenoで動作させるために実装されたオプションです。

このオプションを指定した状態でdeno runを実行すると、カレントディレクトリにpnpmライクなフォーマットでnode_modulesを作成してくれます。

Vite以外にも、node_modulesがないとうまく動かないパッケージを利用したい際は、このオプションの有効化を試してみるとよいと思います。

今後について

Deno本体にnpmパッケージのサポートが入ったことにより、Viteに限らず、Node.jsの様々な資産をDenoから活かすための余地が生まれました。

そのため、現在、様々なパッケージでDenoサポートに向けた動きが見られます。

例えば、Prismaではv4.5.0で正式にDenoサポートが入りました。

これにより、Deno+Prismaで書いたアプリケーションをDeno Deployなどで動かせるようになりました。

https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-deno-deploy

注意点として、Denoにおけるnpmパッケージのサポートは、内部的にはポリフィルをベースに実装されており、場合や用途などによっては、少なからずオーバーヘッドが生じる可能性があるかもしれません。

そのため、SSRなどのパフォーマンスが重視される領域に関しては、依然として、FreshやAleph.jsなどのDenoで実装されたフレームワークなどにも需要が出てくるのではないかと個人的には考えています。

Fresh v1.0

Deno+PreactベースのフレームワークであるFreshのv1.0がリリースされました。

https://deno.com/blog/fresh-is-stable

それに伴い、リポジトリがdenoland Organizationに移管され、FreshはDenoの公式フレームワークという位置づけに変わりました (元々、FreshはDeno Land Inc.のメンバーであるLuca Casonato氏が個人で開発していたプロジェクトでした)

v1.0リリース以降も様々な改善が行われており、プラグインシステムの実装やPreact Signalsのサポートなどが実施されています。

ここではこれらの内容について解説していきます。

プラグインシステム

プラグインシステムは、ユーザがプラグインによってFreshの挙動を拡張するために追加された機能です。

例えば、公式ではTwindプラグインが提供されています。

main.ts
import { start } from "$fresh/server.ts";
import twindPlugin from "$fresh/plugins/twind.ts";

import manifest from "./fresh.gen.ts";
import twindConfig from "./twind.config.ts";

// Twindプラグインを有効化してサーバを起動します。
await start(manifest, { plugins: [twindPlugin(twindConfig)] });

このプラグインを利用することで、twを使わずにクラスを記述できるようになります。

プラグイン適用前:

<div class={tw`font-bold`}>foobar</div>

プラグイン適用後:

<div class="font-bold">foobar</div>

現時点では、プラグインは生成されたHTMLに対するスクリプトやスタイルの注入などの用途での使用が想定されています。

将来的には、このプラグインシステムをさらに拡張し、プラグイン経由でRouteやMiddlewareなどを追加できるようにするなど、より幅広い用途で使用できるようにすることが検討されているようです。

Preact Signalsのサポート

2022/09/06にPreact Signalsが公開されました。

https://preactjs.com/blog/introducing-signals/

Islands間での状態の共有などに活用できることなどから、早速、FreshにもこのPreact Signalsのサポートが入っています。

Fresh公式のinitスクリプトでプロジェクトを生成すると、あらかじめimport_map.jsonにPreact Signalsを読み込むための定義が記述されており、以下のようにしてPreact Signalsを利用することができます。

islands/Counter.tsx
import Button from "../components/Button.tsx";
import { signal } from "@preact/signals";

const count = signal(0);

export default function Counter() {
  return (
    <div class="flex gap-2">
      <p class="flex-grow-1 font-bold text-xl">{count}</p>
      <Button onClick={() => count.value++}>+1</Button>
      <Button onClick={() => count.value--}>-1</Button>
    </div>
  );
}

Aleph.js v1 beta

DenoのフレームワークであるAleph.js v1のbetaバージョンが公開されています。

ここでは、直近で行われている変更のうち主要なものをいくつか紹介いたします。

フレームワークサポートの拡充

まず大きな変更点として、すでにサポートされていたReactやVue.jsなどのフレームワークに加えて、新しくSolidYewのサポートが進められています。

例えば、以下のコマンドを実行することで、Solid向けのプロジェクトを初期化することができます。

$ deno run -A -r https://alephjs.org/init.ts ./aleph --template=solid

これにより、ページコンポーネントなどをSolidで記述することができます。

ただし、Solidについては、ReactやVue.js向けに実装されているuseDataなどの機能はまだサポートされていないようなので、注意が必要そうです。

SSGのサポート

v1.0.0-beta.2でSSGが実装されています。

https://github.com/alephjs/aleph.js/compare/1.0.0-beta.1...1.0.0-beta.2

現時点での使用法としては、まずserver.tsでSSGを有効化する必要があります。(build.ssgオプションにtrueを設定)

server.ts
import { serve } from "aleph/react-server";
import unocss from "./unocss.config.ts";

serve({
  ssr: true,
  router: {
    glob: "./routes/**/*.{ts,tsx}",
  },
  build: {
    ssg: true
  },
  unocss,
});

次にgetStaticPathsexportしたページコンポーネントを用意します。この関数には静的に生成される必要のあるパスの一覧を返却させる必要があります。

ファイル名については、Dynamic routesが適切に機能するよう、動的に置換したい箇所については:<name>.tsx形式で命名する必要があります。(例: routes/todos/:id.tsx)

import { Head, useData } from "aleph/react";
import { getTodoById, getTodoIds } from "../lib/todos.ts";

interface Todo {
  id: number;
  message: string;
  completed: boolean;
}

export async function getStaticPaths() {
  const ids = await getTodoIds();
  return ids.map((x) => `/todos/${x}`);
}

export const data: Data = {
  cacheTtl: 0,
  get: async (_req, ctx) => {
    const { id } = ctx.params;
    const todo = getTodoById(Number(id));
    return Response.json(todo);
  },
};

export default function TodoDetail() {
  const { data: todo } = useData<Todo>();

  return (
    <main>
      <Head>
        <title>{todo.message}</title>
      </Head>
      <div>{todo.message}</div>
    </main>
  );
}

この状態でビルドを実行すると、outputディレクトリにHTMLが生成されます。

$ deno task build 

Ultra v2 beta

ReactベースのフレームワークであるUltraでv2のリリースに向けて開発が進められています。

https://github.com/exhibitionist-digital/ultra

v2に向けた大きな変更点として、Ultra v1はwouterreact-helmetなどの特定のライブラリに強く依存した作りになっていましたが、Ultra v2では様々なエコシステムと柔軟に連携ができるよう改善が進められているようです。

例)

また、Island Architectureのサポートも進められており、今後はFreshとも競合するフレームワークという立ち位置になる可能性がありそうです。

https://github.com/exhibitionist-digital/ultra/tree/v2.0.0-beta.13/examples/with-islands

その他には、サーバがHonoをベースに再実装されており、まだどうなるのかはわからないのですが、もしかしたら、Deno Deploy以外の環境(Cloudflare Workersなど)でも動作させられる可能性が出てくるのかもしれません。

RemixでDenoの公式サポートが決定

RemixでのDenoサポートは元々は実験的サポートという位置づけでしたが、v1.5.0で公式サポートが決まりました。

https://github.com/remix-run/remix/releases/tag/v1.5.0

@remix-run/denoパッケージを利用することで、Remixで開発されたアプリをDenoやDeno Deployなどで動かすことができます。

ただし、現状、依存関係の管理はnpmによって行う想定であり、開発にはNode.jsが必要であることに注意が必要です。

このあたりの背景については、以下のドキュメントで詳しく解説されています。

Nuxt v3でのDenoサポート

Nuxt 3のサーバエンジンであるNitroでは、Nuxt 3を様々なプラットフォームで動作させるために、presetという抽象化レイヤーを提供しています。

現在、このNitroでDeno presetの実装が進んでいるようです。

https://github.com/unjs/nitro/pull/592

これが正式にリリースされれば、Nuxt 3で開発されたアリケーションをDeno Deploy上などで動かせるようになりそうです。

また、Nuxt 3はViteを使っており、Denoのnpmパッケージサポートを利用することにより、ローカル開発もDeno上で行えるようになる可能性もありそうです。

まとめ

この半年間でDenoのnpmパッケージサポートが進むなど、Deno本体で大きな変更が実施されました。Viteに限らず、ESLint/Prettier/stylelint/commitlintなどフロントエンド開発に関わる資産の多くはNode.jsをベースに実装されており、それらを活用できるようになることは大きいことなのではないかと感じています。

また、これらの資産をDenoが提供するパーミッションシステムなどと連携させることで、より安全にそれらのツールを活用できるようになるのではないかと感じています。

その他にも、Fresh v1.0がリリースされ、Denoの公式フレームワークとして扱われるようになったことで、今後さらに開発などが加速していくものと思われます。

今後、Deno Deployが正式にリリースされれば、Freshの需要も少しずつ増していくと思われるため、個人的にはとても注目しています。

Discussion