ricomintea’s diary

好きなものを好きでいる

マスタリングDeno Fresh メモ

⚠️これはドキュメントを読んだメモです。

テンション上げていけという気持ちでマスタリングという名をつけています。

fresh.deno.dev

Deno Freshとは

速度・信頼性・シンプルを掲げて作られた、Deno実装の次世代Webフレームワーク。ビルドステップがなくエッジレンダリングベース。TypeScriptやフロントエンド開発に必要な基本的な機能に設定を必要としないアイランドアーキテクチャを採用していて、デフォルトではクライアントはJavaScriptを必要としない

引用: https://fresh.deno.dev/
Fresh embraces the tried and true design of server side rendering and progressive enhancement on the client side.

よくわかっていない単語集

プロジェクト作成

❯ deno run -A -r https://fresh.deno.dev deno-fresh-project

  🍋 Fresh: the next-gen web framework.

Let's set up your new Fresh project.

Fresh has built in support for styling using Tailwind CSS. Do you want to use this? [y/N] y
Do you use VS Code? [y/N] y
The manifest has been generated for 3 routes and 1 islands.

Project initialized!

Enter your project directory using cd deno-fresh-project.
Run deno task start to start the project. CTRL-C to stop.

Stuck? Join our Discord https://discord.gg/deno

Happy hacking! 🦕

サーバー起動

❯ deno task start
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 3 routes and 1 islands.
Listening on http://localhost:8000/

Hot Module Reloadに対応していて、ファイルの変更保存を検知してブラウザページがリロードされる。

ディレクトリ構造

❯ tree .
.
├── README.md
├── components
│   └── Button.tsx
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│   └── Counter.tsx
├── main.ts
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
├── static
│   ├── favicon.ico
│   └── logo.svg
└── twind.config.ts

5 directories, 14 files

ファイル

deno.json : package.jsonのようなもの

dev.ts : プロジェクトのエントリーポイント。deno.jsonのタスクで指定される

fresh.gen.ts : Freshが生成するマニフェストファイル。アイランドアーキテクチャで読み込みするために使われてそう

import_map.json : Denoでよくライブラリ import/export をするファイル

ディレクト

routes : ファイルシステムベースのルーティング。アイランドアーキテクチャな分そのまま配信しているわけではなさそう

islands : インタラクティブアイランドを置くディレクト

components : コンポーネントを置く場所ではあるけどislandsとの違いは何だろう?

static : 静的ファイルを置くディレクト

ルート作成

routes ディレクトリ内に tsx ファイルを追加するとルーティングが追加される。

ドキュメントの例だと AboutPage という名前で関数を定義しているが default export で出していればなんでもいい。

引用: https://fresh.deno.dev/docs/getting-started/create-a-route

// routes/about.tsx

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>This is the about page.</p>
    </main>
  );
}

ダイナミックルーティング

[propName].tsx でファイル名を作成してページコンポーネントの引数でpropNameを受け取ることができる。

引用: https://fresh.deno.dev/docs/getting-started/dynamic-routes

// routes/[name].tsx

import { PageProps } from "$fresh/server.ts";

export default function GreetPage(props: PageProps) {
  const { name } = props.params;
  return (
    <main>
      <p>Greetings to you, {name}!</p>
    </main>);
}

ファイルシステムルーティングって

仕様もしくはそういうフレームワークあるのかな。

型で保護を受けたいみたいな視点でいうと難しさもあるらしい。

zenn.dev

カスタムハンドラー

ルートファイルの中で handler オブジェクトを名前付きエクスポートして適用する。 handler オブジェクトはHTTPリクエストメソッド毎に定義ができる。

⚠️ Next.jsと若干違うところ

Next.jsでは getStatisPropsgetServerSideProps といった関数を定義することで、リクエストに対してハンドラーのようなものを定義できる。

Next.jsでは getServerSideProps 後にページコンポーネントの処理に移る。対してFreshではハンドラー内で受け取る ctx を使って、ページコンポーネントのレンダー結果をレスポンスに含める。

引用: https://fresh.deno.dev/docs/getting-started/custom-handlers

// routes/about.tsx

import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    const resp = await ctx.render();
    resp.headers.set("X-Custom-Header", "Hello");
    return resp;
  },
};

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>This is the about page.</p>
    </main>);
}

ページコンポーネントではなく、JSONやファイルなどのデータを返したい場合は routes/api ディレクトリ以下でハンドラーのみ定義するのがいい。

データフェッチ

ハンドラー内で取得してきて ctx.render を呼び出すときに渡してやるといい。

フォーム送信

ReactとかVue使ってるとフォームもAPIで送信しがちだからハンドラーの方に GET or POST で返すの逆に珍しい。

クライアント側にJavaScript送らなくていいって言っているのこの辺りだよな。

サーバーサイドでレンダリングするとサーバーの負荷が高まる。ブラウザでレンダリングするとクライアントにJavaScriptがたくさん必要になる。サーバーサイドレンダリングからハイドレーションみたいなやり方でも、クライアント側でJavaScriptが多く必要な上に、サーバーとクライアントで同じような処理をするところが無駄無駄。

Freshのこのやり方はエッジサーバーで負荷分散の能力が高まってる前提があって、Denoがやり始めたCDN EdgeのDeno Deployとのシナジーがすごい。

deno.com

zenn.dev

フォーム送信についてブラウザがいいシステムを持ってる話は、ほんとにいいなと思う。扱っているシステムが素で持っている(追加されている)機能が結構強いものあったりする。TypeScriptに対するJavaScript本体とか。

引用: https://fresh.deno.dev/docs/getting-started/form-submissions

// routes/search.tsx

import { Handlers, PageProps } from "$fresh/server.ts";

const NAMES = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Frank"];

interface Data {
  results: string[];
  query: string;
}

export const handler: Handlers<Data> = {
  GET(req, ctx) {
    const url = new URL(req.url);
    const query = url.searchParams.get("q") || "";
    const results = NAMES.filter((name) => name.includes(query));
    return ctx.render({ results, query });
  },
};

export default function Page({ data }: PageProps<Data>) {
  const { results, query } = data;
  return (
    <div>
      <form>
        <input type="text" name="q" value={query} />
        <button type="submit">Search</button>
      </form>
      <ul>
        {results.map((name) => <li key={name}>{name}</li>)}
      </ul>
    </div>);
}

アイランドアーキテクチャの話

Deno Freshを使うと静的なページやユーザーアクションのないページに関してはJavaScriptのバンドルサイズ0が実現できる。けど現実にはそんなページはほとんどなくて、何かの機能を提供するボタンやリッチアニメーションなどの装飾のために少しだけJavaScriptが必要になる。

こういう静的なコンテンツの多いページの中に、ぽつぽつと島のようにJavaScriptが必要な要素があるみたいな形をアイランドアーキテクチャと呼んでるらしい。

Deno Freshでの islands ディレクトリで定義するコンポーネントはクライアントでレンダリング(というかハイドレーション)される。これパーシャルハイドレーション。

マークアップのみのコンポーネントであれば components ディレクトリに定義して、状態を持ったりするコンポーネントislands ディレクトリに定義することになりそう?

マークアップのみのコンポーネントであっても islands 内のコンポーネントから使われる場合もある。それを考慮するために $fresh/runtime.tsIS_BROWSER っていうフラグを持ってる。

import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";

export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
  return (
    <button
      {...props}
      disabled={!IS_BROWSER || props.disabled}
      class="px-2 py-1 border(gray-100 2) hover:bg-gray-200"
    />
  );
}

他にない感じの処理だ。

デプロイ先

GitHubにプッシュしてDeno Deploy使うといいよって。Cloudflare Workersとかもよく聞く。