Deno のWebフレームワーク Fresh で遊んでみた

先日、DenoのWebフレームワーク<a href="https://fresh.deno.dev/">Fresh 1.0</a>が安定版をリリース。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/556b276c-59fe-70d7-4bb9-a7a41ac0bb42.png)

可愛い..//

> - Just-in-time rendering on the edge.
> - Island based client hydration for maximum interactivity.
> - Zero runtime overhead: no JS is shipped to the client by default.
> - No build step.
> - No configuration necessary.
> - TypeScript support out of the box.

和訳すると、

 - 　エッジでのJust-in-timeレンダリング
 - 　ビルドの工程が不要
 - 　デフォルトではクライアントにJavaScriptを用いないため、ランタイムオーバーヘッドがゼロ
 - 　高いインタラクティブ性を実現するためアイランドベースのクライアントハイドレーションを採用
 - 　コンフィグレーション不要
 - 　TypeScriptをサポート

しているらしい。

https://fresh.deno.dev/

超絶砕いて言うと、HTTPリクエスト時に即時レンダリングされるため、ビルドという概念はなく、基本コンテンツはサーバーサイドでレンダリングされるため、クライアントにjsを送信せず（そのおかげで画面表示パフォーマンスは良い）、インタラクティブ性を持たせる場合は一部コンテンツにjsのEvent listenerを登録する<a href="https://jasonformat.com/islands-architecture/">Islands Architecture</a>を採用しており、もちろんts対応。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/336688f9-31fd-ed70-9630-970e64c6a36e.png" width="500">

意味がわかるようなわからないような・・・とりあえず実際に手を動かして遊んでみる。

## 成果物

https://github.com/yamachita0109/fresh_typing

https://yamachita0109-fresh-typing.deno.dev/

## 環境構築

Freshを使用するにはDeno v1.22.3 以降のVersionが必要となるため、まずはInstallする。
```bash
# install
$ curl -fsSL https://deno.land/install.sh | sh 
# Version確認
$ deno --version
deno 1.23.4 (release, x86_64-apple-darwin)
v8 10.4.132.8
typescript 4.7.4
```

DenoのInstallが完了したらFreshのProjectを作成。
```bash
$ deno run -A --no-check https://raw.githubusercontent.com/lucacasonato/fresh/main/init.ts fresh
```

## Folder Structure

```bash
$ tree
.fresh/
├── README.md
├── 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
└── utils
    └── twind.ts
```

### deno.json

所謂Denoの設定ファイル。
npm / yarnで言うpackage.jsonとほぼ同じような内容。

### dev.ts / main.ts

開発、本番環境のエントリーポイント。

### fresh.gen.ts

routes と islands の情報を含むマニフェストファイル。
※ 正直よくわからない・・Islands Architectureにおける動的な部分を定義したファイルかな？
routes と islands 実装時に自動で更新されるため、修正は不要。

### import_map.json

<a href="https://deno.land/manual/linking_to_external_code/import_maps">Denoのパッケージ管理</a>
※ <a href="https://deno.land/manual/examples/manage_dependencies">deps.ts</a>でやるものと思っていたら、Deno v1.8からimport　mapsがサポートされた様子。

### islands/

Islands ArchitectureでのIslandsコンポーネントを実装する階層。

```islands/Counter.tsx
/** @jsx h */
import { h } from "preact";
import { useState } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { tw } from "@twind";

interface CounterProps {
  start: number;
}

export default function Counter(props: CounterProps) {
  const [count, setCount] = useState(props.start);
  const btn = tw`px-2 py-1 border(gray-100 1) hover:bg-gray-200`;
  return (
    <div class={tw`flex gap-2 w-full`}>
      <p class={tw`flex-grow-1 font-bold text-xl`}>{count}</p>
      <button
        class={btn}
        onClick={() => setCount(count - 1)}
        disabled={!IS_BROWSER}
      >
        -1
      </button>
      <button
        class={btn}
        onClick={() => setCount(count + 1)}
        disabled={!IS_BROWSER}
      >
        +1
      </button>
    </div>
  );
}
```
デフォルトでは、クリックするとカウントアップ/ダウンされる処理が実装されている。

### routes/

Nuxtのpagesのようなファイルベースのルーティング。
デフォルトでは
```bash
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
```
のように3種定義されており、
それぞれ
 - http://localhost:8000/ : routes/index.tsx
 - http://localhost:8000/{param} : routes/[name].tsx
 - http://localhost:8000/api/joke : routes/api/joke.tsx

に対応している。

### static/

静的ファイル。

### utils/

ユーティリティ。
環境構築時に脳死でEnter押していたら
```bash
Do you want to use 'twind' (https://twind.dev/) for styling? [y/N] y
```
としていたため、<a href="https://tailwindcss.com/">Tailwind CSS</a>がデフォルトで入っている。嬉しい。

## Coding

いつもながら、タイピングゲームでも実装してみようかと。

### タイピング用のお題取得

とある偉人の名言を適当にとってくるAPIを実行。

```routes/api/advice.ts
import { HandlerContext } from "$fresh/server.ts";

interface Advice {
  slip: {
    id: number,
    advice: string
  }
}

export const handler = async (_req: Request, _ctx: HandlerContext): Promise<Response> => {
  const res = await fetch("https://api.adviceslip.com/advice");
  if (res.status === 404) {
    throw Error;
  }
  const advice: Advice = await res.json();
  return Response.json(advice.slip);
};
```

### TOPを軽くいじる

```routes/index.tsx
/** @jsx h */
import { h } from "preact";
import { tw } from "@twind";
import Counter from "../islands/Game.tsx";

export default function Home() {
  return (
    <div class={tw`p-4 mx-auto max-w-screen-md text-center`}>
      <img
        src="/logo.png"
        class={tw`rounded-full h-64 flex items-center m-0 m-auto`}
        alt="logo"
      />
      <p class={tw`my-6`}>
        Let's Typing.
      </p>
      <Game />
    </div>
  );
}
```

特に触れることはなし。
※Tailwind CSSのStyleがなかなか適応されなくて、なんでかなーと思ったら`{tw ***}`の形式で実装できていなかったのは秘密。

### island/Game.tsx

もう少しマシな書き方はありそうだが、

```island/Game.tsx
/** @jsx h */
import { h } from "preact";
import { useState } from "preact/hooks";
import { get } from "apis/advice.ts";
import { tw } from "@twind";

export default function Game() {
  const [active, setActive] = useState(false);
  const [question, setQuestion] = useState('-');
  const [num, setNum] = useState(0);
  const [timer, setTimer] = useState(0);

  const methods = {
    clickStart: async () => {
      setActive(true);
      const res = await get();
      setQuestion(res.advice);
      methods.startTimer();
    },

    inputForm: (e: InputEvent) => {
      const value = (e.target as HTMLInputElement).value;
      if (value !== question) return;
      methods.stopTimer();
      setActive(false);
    },

    startTimer: () => {
      const start = Date.now(), accum = 0;
      setTimer(setInterval(() => { setNum(accum + (Date.now() - start) * 0.001) }, 10));
    },

    stopTimer: () => {
      clearInterval(timer);
    },
  }

  return (
    <div>
      <div class={tw`mb-10`}>
        <button
          class={tw`inline-flex items-center justify-center w-full px-6 py-3 mb-2 text-lg text-white
            bg-green-500 rounded-md hover:bg-green-400 sm:w-auto sm:mb-0 disabled:bg-gray-300`}
          disabled={ active }
          onClick={ methods.clickStart }
        >
          Start.
        </button>
      </div>

      <div class={tw`mb-10`}>
        <strong class={tw`text-3xl font-thin leading-none text-neutral-600 lg:text-4xl`}>
          { num.toFixed(2) }
        </strong>
      </div>

      <div class={tw`mb-10`}>
        <strong class={tw`text-3xl font-thin leading-none text-neutral-600 lg:text-4xl`}>
          { question }
        </strong>
      </div>

      <div class={tw`mb-10`}>
        <input
          type="text"
          class={tw`relative outline-none rounded py-3 px-3 w-full bg-white shadow text-sm
            text-gray-700 placeholder-gray-400 focus:outline-none focus:shadow-outline`}
          placeholder="Let's Typing."
          disabled={ !active }
          onInput={ methods.inputForm }
        />
      </div>
    </div>
  );
}
```

Event listenerがピュアなjsと同じ・・・（Vue / Reactのように専用の記法があるのかな・・）

状態管理にはPreact `useState`フックを使用。
```ts
import { h } from "preact";
import { useState } from "preact/hooks";
const [active, setActive] = useState(false);
```

例
```ts
  const [flg, setFlg] = useState(false);
  return (
    <div>
      <button
        disabled={ flg }
        onClick={() => setFlg(true)}
      >
    </div>
  )
```

これはNG
```ts
  let flg = false;
  return (
    <div>
      <button
        disabled={ flg }
        onClick={() => flg = true}
      >
    </div>
  )
```

### utils/apis/advice.ts

api クライアント.

```utils/apis/advice.ts
import axiod from "https://deno.land/x/axiod@0.26.1/mod.ts";

const ENDPOINT = "/api/advice";

interface Advice {
  id: number,
  advice: string
}

export async function get(): Promise<Advice> {
  const res = await axiod.get(ENDPOINT);
  return res.data;
}
```

しっかり作り込むのであれば、`http client`を実装して、mock切り替えなど実装した方が良いがそこまでリッチにする意味はないので割愛。

import_map.jsonにもしっかり追加。
```import_map.json
{
  "imports": {
+    "apis/": "./utils/apis/",
    "$fresh/": "https://raw.githubusercontent.com/lucacasonato/fresh/main/",
    "preact": "https://esm.sh/preact@10.8.2",
    "preact/": "https://esm.sh/preact@10.8.2/",
    "preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.0?external=preact",
    "@twind": "./utils/twind.ts",
    "twind": "https://esm.sh/twind@0.16.17",
    "twind/": "https://esm.sh/twind@0.16.17/"
  }
}
```

## Deploy

Deno Deployを使用。

https://deno.com/deploy

こんな感じで設定して

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/4825d093-5ba4-6133-daf2-c949b45f540d.png)

ぽちぽちすれば

https://yamachita0109-fresh-typing.deno.dev/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/da7aaaab-4103-3a16-d18a-dd3a2df5253e.png)

はい完了。

## まとめ

ちょっと触った感じ、現時点では微妙・・・
環境構築やドキュメント量も少なく理解しやすいが、インタラクティブ性が低い。（まぁそういうアーキテクチャだし。）
Jamstackっぽいので、ブログサイトとかには適しているかも。

とわ言え、まだ1.0がリリースされたばかり、今後に期待ですな。