Deno のフレームワーク「Fresh」で爆速開発

Mar 21st, 2023 typescript deno fresh

はじめに

この記事はジーズアカデミー Advent Calendar 2022の 1 日目である.

JavaScript 及び TypeScript のランタイム環境である Deno のフレームワークである Fresh がすばらしいので推す.

今回のネタ

成果物

ソース

https://github.com/taroosg/fresh-janken

動作環境

対象者

もくじ

Deno のインストール

コマンド実行するだけ.mac だったら多分 Homebrew を使うのが一番楽.

https://deno.land/manual@v1.28.2/getting_started/installation

Fresh のプロジェクト作成

コマンド一発で OK.TailWindCSS を自動的に準備してくれるので使用する場合は y ですすめる.VS Code の設定もしてくれるのでこちらも必要に応じて y ですすめる.

deno run -A -r https://fresh.deno.dev PROJECT_NAME

  🍋 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 PROJECT_NAME.
Run deno task start to start the project. CTRL-C to stop.

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

Happy hacking! 🦕

ドキュメントが親切なので見ながら進めれば間違いない.

https://fresh.deno.dev/docs/getting-started/create-a-project

プロジェクトを作成したら以下コマンドで立ち上げる.停止させるときは ctrl + c で OK.

deno task start

立ち上げるとカウンターの画面が表示される.このページは index.tsx で,ページ内で islands/Countercomponents/Button を読み込んでいる.

ルーティングとページの作成

早速プロダクトの実装を進めていく.本記事ではおみくじとじゃんけんの処理を実装するが,Fresh はフルスタックのフレームワークであり,フロントエンドとサーバサイド双方の実装を行うことができる.

おみくじはフロントエンドの実装,じゃんけんはサーバサイドの実装を行う.

プロジェクト内は以下の構成となっている.

.
├── README.md
├── components
│   └── Button.tsx
├── deno.json
├── deno.lock
├── 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

ページを作成する際には routes 内にファイルを作成するだけで良い.Fresh はファイルベースのルーティングとなっており,ディレクトリとファイルでルーティングを行う.Next.js で開発経験のある人には馴染み深い.

以下のファイルを作成しよう.

続いて,それぞれのファイルに下記の内容を記述する.

// routes/omikuji_page.tsx

import { Head } from "$fresh/runtime.ts";

export default function Omikuji_page() {
  return (
    <>
      <Head>
        <title>Omikuji</title>
      </Head>
      <h1>おみくじのページ</h1>
    </>
  );
}
// routes/janken_page.tsx

import { Head } from "$fresh/runtime.ts";

export default function Janken_page() {
  return (
    <>
      <Head>
        <title>Janken</title>
      </Head>
      <h1>じゃんけんのページ</h1>
    </>
  );
}

このように,Fresh ではコンポーネントの形式で画面の実装を行う.React や Next.js を扱った経験のある人ならすぐに慣れることができる.

コードを書いたら下記の URL にアクセスして動作を確認しておこう(それぞれ「おみくじのページ」と「じゃんけんのページ」が表示されれば OK).

おみくじの実装(フロントエンド)

続いて,おみくじの処理を書いてみよう.

Fresh では,基本的に JavaScript を読み込まず,動的な処理が記述されているコンポーネントが含まれる場合のみ読み込む.

islands ディレクトリ以下におみくじ用のコンポーネントである omikuji.tsx を作成して以下の内容を記述しよう.

// islands/omikuji.tsx

import { useState } from "preact/hooks";

export default function Omikuji() {
  const getOmikuji = () =>
    ["大吉", "中吉", "小吉", "凶", "大凶"][~~(Math.random() * 5)];

  const [omikujiResult, setOmikujiResult] = useState("...");

  return (
    <>
      <button onClick={() => setOmikujiResult(getOmikuji())}>
        おみくじをひく
      </button>
      <p>今日の運勢は{omikujiResult}です</p>
    </>
  );
}

routes/omikuji_page.tsx の内容を以下のように編集する.

// routes/omikuji_page.tsx

import { Head } from "$fresh/runtime.ts";
import Omikuji from "../islands/omikuji.tsx";

export default function Omikuji_page() {
  return (
    <>
      <Head>
        <title>Omikuji</title>
      </Head>
      <h1>おみくじのページ</h1>
      <Omikuji />
    </>
  );
}

localhost:8000/omikuji_page にアクセスし,「おみくじを引く」ボタンをクリックしてみよう.毎回ランダムにおみくじの結果が表示されれば OK.

これらの実装も React などで開発を行った経験があればスムーズに進められるだろう.

じゃんけんの実装(サーバサイド)

続いて,じゃんけんの処理を実装する.

じゃんけんはプルダウンで自分の手を選択し,送信ボタンクリックでサーバに自分の手を POST で送信して結果を画面に表示する実装とする.

サーバサイドでの動きは以下の流れとなる.

  1. GET や POST に応じてハンドラーを記述しておく.async GET(){...} async POST(){...} のように記述し,それぞれ GET と POST でリクエストを受けた際に{}内のコードが実行される.
  2. ハンドラーは ctx.render(data) を返す.ここに入力した値がコンポーネントに渡される.
  3. コンポーネントは { data } の形で入力値を受け取り,コンポーネント内で使用することができる.

上記全てはサーバで実行され,構成された HTML のみがブラウザに渡される SSR の仕組みとなっている.

routes/janken_page.tsx を以下のように編集する.

// routes/janken_page.tsx

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

// じゃんけんの結果をまとめる型
interface JankenResult {
  userHand: string;
  computerHand: string;
  winLose: string;
}

// じゃんけん関連の関数
const getComputerHand = () => [0, 1, 2][~~(Math.random() * 3)];

const getWinLose = (userHand: number | undefined, computerHand: number) =>
  [
    ["draw", "win", "lose"],
    ["lose", "draw", "win"],
    ["win", "lose", "draw"],
  ][userHand ?? 0][computerHand];

const getJankenHand = (jankenIndex: number) =>
  ["グー", "チョキ", "パー"][jankenIndex];

// ハンドラーの実装
export const handler: Handlers<JankenResult> = {
  // GETでページを開いた際には「待機中」のメッセージをコンポーネントに渡す.
  async GET(_, ctx) {
    const result: JankenResult = {
      userHand: "待機中",
      computerHand: "待機中",
      winLose: "待機中",
    };
    return ctx.render(result);
  },
  // POSTでリクエストされた場合(じゃんけんの手が送信されてきた場合)はじゃんけんの処理を実行して結果をコンポーネントに渡す.
  async POST(req, ctx) {
    const formData = await req.formData();
    const userHandIndex = Number(formData.get("user_hand"));
    const userHand = getJankenHand(userHandIndex);
    const computerHandIndex = getComputerHand();
    const computerHand = getJankenHand(computerHandIndex);
    const winLose = getWinLose(userHandIndex, computerHandIndex);
    const result = { userHand, computerHand, winLose };
    return ctx.render(result);
  },
};

// コンポーネントはじゃんけんの結果を受け取る
export default function Janken_page({ data }: { data: JankenResult }) {
  return (
    <>
      <Head>
        <title>Janken</title>
      </Head>
      <h1>じゃんけんのページ</h1>
      <h2>じゃんけんをする</h2>
      <form action="" method="POST">
        <select
          name="user_hand"
          defaultValue={["グー", "チョキ", "パー"]
            .indexOf(data.userHand)
            .toString()}
        >
          {["グー", "チョキ", "パー"].map((x, i) => (
            <option value={i}>{x}</option>
          ))}
        </select>
        <button type="submit">送信</button>
      </form>
      <h2>結果</h2>
      <p>自分の手:{data.userHand}</p>
      <p>相手の手:{data.computerHand}</p>
      <p>結果:{data.winLose}</p>
    </>
  );
}

コードを記述したら動作確認しよう.localhost:8000/janken_page にアクセスし,自分の手を選択して送信して結果が表示されれば OK!

Fresh でアプリケーションを実装する際には,このようなサーバサイドの処理がメインとなるだろう.ハンドラーを実装し,その中で外部 API や DB とのやり取りのコードを記述することで実装を進められるだろう.

デプロイ

デプロイには Deno 公式が提供する Deno Deploy を使用する.GitHub 連携できるためとても簡単である.

まずは実装した Fresh のコードを GitHub に push しておく.

Deno Deploy にアクセスし,GitHub アカウントで認証しておく.

https://deno.com/deploy

サインインしたら,プロジェクトを作成する.New Project ボタンをクリックすると画面が変わるので,GitHub リポジトリを選択する.「Add Repository」をクリックすれば OK.

「Add Repository」をクリックすると自動的に GitHub のページが開く.連携したいリポジトリ(Fresh のコードを push したリポジトリ)を選択し,Save をクリックする.

「Install Success」が表示されたら Deno Deploy のページへ戻り,連携するリポジトリを選択する.「main ブランチ」→「Automatic」→「main.ts」の順にクリックして「Link」をクリックする.

少し待つと画面が変わるので,「View」をクリックするとデプロイ先を開く事ができる.

デプロイ先の URL の後ろに /omikuji_page/janken_page をつけてページを開き,実装した内容が動作していることを確認しよう!

ブラウザの操作だけで完結!すごい!

まとめ

本記事では Fresh を使用した基本的な実装を行い,デプロイまでの手順を解説した.

Deno Deploy を含めて TypeScript での開発からデプロイまで全てを賄えるため非常に強力である.DB や外部 API との連携で完結する実装であればだいたい事足りそうな予感.個人開発とか全部これでいい気がしている.

興味出たらぜひ触ってみよう!

今回は以上だ(`・ω・)b