LoginSignup
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-19

先日、DenoのWebフレームワークFresh 1.0が安定版をリリース。

image.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をサポート

しているらしい。

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

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

成果物

環境構築

Freshを使用するにはDeno v1.22.3 以降のVersionが必要となるため、まずはInstallする。

# 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を作成。

$ deno run -A --no-check https://raw.githubusercontent.com/lucacasonato/fresh/main/init.ts fresh

Folder Structure

$ 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

Denoのパッケージ管理
deps.tsでやるものと思っていたら、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のようなファイルベースのルーティング。
デフォルトでは

├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx

のように3種定義されており、
それぞれ

に対応している。

static/

静的ファイル。

utils/

ユーティリティ。
環境構築時に脳死でEnter押していたら

Do you want to use 'twind' (https://twind.dev/) for styling? [y/N] y

としていたため、Tailwind CSSがデフォルトで入っている。嬉しい。

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フックを使用。

import { h } from "preact";
import { useState } from "preact/hooks";
const [active, setActive] = useState(false);

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

これはNG

  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を使用。

こんな感じで設定して

image.png

ぽちぽちすれば

image.png

はい完了。

まとめ

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4