[Deno] Fresh入門ハンズオン ～ プロジェクト作成からデプロイまで

[Fresh](https://fresh.deno.dev/)はDeno社が作成しているWebフレームワークです。

https://fresh.deno.dev/

https://github.com/denoland/fresh

FreshはDenoでフロントエンドを作成する際の選択肢として一番最初に挙がるフレームワークだと思います。登場からもうすぐ1年経つところで、色々改善も入り安定して使えるようになってきたようです。

この記事では、DenoとFreshを使用して、プロジェクト作成から本番環境へのデプロイまでの流れを紹介したいと思います。

# Freshプロジェクトの作成

#### Denoのインストール

Freshは最新版のDenoで動作します。まずDenoをインストールする必要があります。

https://deno.com/manual/getting_started/installation

```shell:Denoをインストール
# mac/linuxの場合
> curl -fsSL https://deno.land/x/install/install.sh | sh
# windowsの場合
> irm https://deno.land/install.ps1 | iex
```

```shell:Denoを最新版にアップデート
> deno upgrade
```

#### Freshプロジェクト作成

Freshプロジェクトは以下のコマンドで作成できます。

```shell:Freshプロジェクトの初期化 (ディレクトリ名指定)
# my-appディレクトリを作成し、その中にFreshプロジェクトを作成
> deno run -A -r https://fresh.deno.dev my-app
```

なお、ディレクトリを切らずに、カレントディレクトリをFreshプロジェクトにしたい場合は、最後を`.`に変えます。

```shell:Freshプロジェクトの初期化 (カレントディレクトリ)
# カレントディレクトリにFreshプロジェクトを作成
> deno run -A -r https://fresh.deno.dev .
```

実行すると、質問が2つか3つ聞かれます。
「（ディレクトリにファイルがある場合）上書きしてもよいか」
「Tailwind CSSを使用するか」
「VS Code用の設定ファイルを生成するか」

特に問題なければすべて「`y`」を入力しましょう。

```shell
# 指定したディレクトリに既にファイルが存在する。上書きしてもよいか？
The target directory is not empty (files could get overwritten). Do you want to continue anyway? [y/N]
# Tailwind CSSを使用するかどうか
Fresh has built in support for styling using Tailwind CSS. Do you want to use this? [y/N]
# VS Code用の設定ファイルを作るかどうか
Do you use VS Code? [y/N] y
```

初期化が終了すると、以下のようなメッセージが表示されます。

```
Project initialized!

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

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

Happy hacking! 🦕
```

#### サーバーの起動

新しくディレクトリを作成された方は、`cd my-app`などで当該ディレクトリに移動したのち、以下のコマンドを打つとサーバーが起動します。

```shell:サーバー起動
> deno task start
```

サーバーを起動した状態で http://localhost:8000 に移動すると、以下のような画面になっているはずです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/93e40e4b-73f4-0b98-7738-686abd6ed174.png)

ファイルを変更すると、サーバーは自動で再起動します。またホットリロードにも対応しているため、ブラウザ側でも自動で再読み込みされます。

サーバーを停止するにはターミナルで`ctrl-c`を入力します。

# ルーティングの設置

Freshはファイルシステムベースのルーティングを採用しています。ルーティングは`routes/`ディレクトリの中が対象となります。

```:ルーティングの一例
routes/index.tsx          --> / (ルート)にアクセスした時に実行される
routes/hello.tsx          --> /helloにアクセスした時に実行される
routes/users/[name].tsx   --> /users/bob や /users/alice にアクセスした時に実行される
routes/hello/[...path].ts --> /hello/foo や /hello/foo/bar など、hello/以下にアクセスした時に実行される
```

`[name].tsx`のように、角括弧で囲まれたパスはワイルドカードとして使用されます。

```tsx:[name].tsx (例)
import { PageProps } from "$fresh/server.ts";

export default function Greet(props: PageProps) {
  // パス名のワイルドカードは`props.params.name`で受け取ることができる。
  return <div>Hello {props.params.name}</div>;
}
```

#### 非同期処理を挟む

`routes/`ディレクトリ内のコンポーネントはSSRされます。SSRの際に、API呼び出しなどの非同期処理を挟みたい場合は、`handler`関数を作成してエクスポートすることで対応可能です。

```tsx
import { HandlerContext, PageProps } from "$fresh/server.ts";

interface User {
  name: string;
}

export const handler = (req: Request, ctx: HandlerContext): Response => {
  const res = await fetch("https://example.com/");
  const userData = await res.json();
  // ctx.render()を呼び出すとSSRされる
  return ctx.render(userData);
};

export default function Page(props: PageProps<User>) {
  // props.dataの中に、handlerで取得したデータが入っている。
  return <div>{props.data.name}</div>
}
```

#### HTML以外のレスポンスを返す

routes内のファイルから`handler`関数をエクスポートすることで、HTML以外のレスポンスを返すことができます。

```tsx
import { HandlerContext } from "$fresh/server.ts";

export const handler = (req: Request, ctx: HandlerContext): Response => {
  // // ctx.render()を使用して、HTMLをレンダリングした結果を返すこともできる。
  // if (new URL(req.url).searchParams.has("html")) {
  //   return ctx.render();
  // }
  return Response.json({ hello: "world" })
};
```

#### 特殊なルーティング

`config`という名前の変数をエクスポートし、そこで`routeOverride`を設定することで、ファイルシステムベースではない特殊なルーティングを行うことができます。

```ts:routes/x.ts
// https://fresh.deno.dev/docs/concepts/routing の例から
import { RouteConfig } from "$fresh/server.ts";

export const config: RouteConfig = {
  routeOverride: "/x/:module@:version/:path*",
};
```

# islandによるクライアントサイドJSの実行

Freshではislandsアーキテクチャを採用しています。

:::note
**islandsアーキテクチャとは？**

Freshは、デフォルトではSSR (Server-Side-Rendering) するMPA (Multi-Page-Application)として動作します。
つまり、**クライアントサイドにおけるJavaScript実行はデフォルトでは0**で、ページ遷移なども通常のHTTPリクエストとして処理されます。

ただし、FreshではWebページの**特定の部分のみ、ブラウザにJavaScriptを配信して動きを付けることができます**。
この「特定の部分」のことを通称`islands`と呼びます。また、こうした方式をislandsアーキテクチャと呼びます。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/0e5a396e-7a0e-4b47-48a1-2290b79826ca.png)
> https://deno.com/blog/intro-to-islands#what-are-islands より。

▲Webページの一部分のコンポーネント(=islands)のみ、JavaScriptを使用してインタラクティブにする。それ以外の部分は完全に静的なHTMLになる。
:::

Freshでは、**`islands/`ディレクトリに置いたコンポーネントがislandsとして扱われます**。

`islands/`ディレクトリのコンポーネントは、**まずサーバー側でSSRされ、次にブラウザ側でもう一度レンダリングされます**(=hydration)。

こうすることで、初期表示ではSSRによって高速に表示しつつ、JSの読み込みが完了したらイベントハンドラなどが流し込まれるようになっています。

逆に言うと、ブラウザ側で実行すべき`onClick`などが含まれるコードは、`islands/`ディレクトリか、`islands/`内からimportされるファイルに記述しなければなりません。

```tsx:islands/Counter.tsx (例)
import { useState } from "preact/hooks";

// islandsディレクトリにあるため、イベントハンドラ(onClickなど)が設定できる。
export default function Counter() {
  const [count, setCount] = useState(props.start);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}
```

#### Preactのhooks

Freshでは、内部で[Preact](https://preactjs.com/)を使用してJSXをレンダリングします。

Preactは`useState()`や`useEffect()`、`useRef()`などのhooksをサポートしています。これらは、`preact/hooks`からインポートすることができます。これらを使用して、ブラウザ側で状態管理などを行うことができます。

```tsx
import { useState, useEffect, useRef } from "preact/hooks";
```

これらの使い方は、詳しくは[Preactのドキュメント](https://preactjs.com/guide/v10/hooks)を参照してください。

また、Preactには`signals`も用意されています。これは`useState()`に似ていますが、islands間で状態を共有するときに使えます。

```tsx
import { signal } from "@preact/signals";
```

こちらについても、詳しくは[Preactのドキュメント](https://preactjs.com/guide/v10/signals)や[Freshのドキュメント](https://fresh-with-signals.deno.dev/)を参照してください。


#### `IS_BROWSER`変数

上記の通り、`islands/`ディレクトリのコードはサーバーとブラウザの両方で実行されます。
ただし、どちらか片方だけで実行したい処理がある場合は、`IS_BROWSER`という変数をimportして使うことができます。
例えばブラウザ側だけでしか動かないライブラリがある場合、`IS_BROWSER`で条件分岐して動的importするなどの回避策があります。

```tsx
import { IS_BROWSER } from "$fresh/runtime.ts";
if (IS_BROWSER) {
  // ブラウザ側だけで実行したい処理
}
```

#### button要素やinput要素のTips

イベントハンドラを設定する際、HTMLが配信されてからJavaScriptが配信されイベントハンドラが有効になるまで、2～3秒程度のタイムラグが発生することがあります。(コールドスタート時など)
この間に`<input>`要素や`<button>`要素をユーザーが操作してしまうと、実際の値とstate管理の値が食い違ってしまい、変な挙動をするように見える場合があります。

これを防ぐには、`<input>`要素や`<button>`要素に`disabled={!IS_BROWSER}`という属性を付与するとよいです。

```diff_tsx:islands/Input.tsx (例)
  export default function Input() {
    const [data, setData] = useState("");
    return (
      <input
        type="text"
        onClick={(e) => setData(e.currentTarget.value)}
+       disabled={!IS_BROWSER}
      />
    );
  }
```

これを設定すると、

- SSR時：`disabled=true`となり、入力を受け付けなくなる
- ブラウザにJSが読み込まれ、イベントハンドラが設定される時：`disabled=false`となり、入力を受け付ける

という形で、挙動を安定させることができます。

# 汎用コンポーネントの作成

`components/`ディレクトリ内に任意のコンポーネントを作成し、他のディレクトリからimportすることができます。

```tsx:components/Header.tsx (例)
export function Header() {
  return <header>Hello!</header>
}
```

このディレクトリに置いたコンポーネントは、`routes/`からも`islands/`からもimportすることができます。必要に応じて`IS_BROWSER`による条件分岐をしましょう。

#### twindによるスタイリング

Freshではtwindプラグインが用意されており、Tailwind CSSを利用したスタイリングが可能になっています。

```tsx:components/Header.tsx (例)
export function Header() {
  return <header class="p-1 m-1 text-red-500 bg-blue-500">Hello!</header>
}
```

なお、Fresh 1.0系の場合、デフォルトではtwind v0プラグインが同梱されています。
twind v1に切り替えた方が使える構文も多いようなので、そちらに切り替えることも検討する価値があるかもしれません。

詳しくは [[Fresh 1.1.4~] twind v1を使用する方法](https://qiita.com/access3151fq/items/71dcc07978b7e61fe263) をどうぞ。

https://qiita.com/access3151fq/items/71dcc07978b7e61fe263

# 静的ファイルの配信

画像などの静的ファイルは、`static/`ディレクトリに配置します。

```:staticディレクトリの例
static/favicon.ico --> /favicon.icoにアクセスした時に読み込まれる
static/images/picture.png --> /images/picture.pngにアクセスした時に読み込まれる
```

# その他のファイル

上記で紹介したディレクトリ以外にも、ファイルが存在します。

|パス名|役割|
|---|---|
`main.ts`|本番環境で使用されるエントリポイントです。
`dev.ts`|開発環境で使用されるエントリポイントです。
`import_map.json`|ライブラリをimportする際のエイリアスを定義できるファイルです。ファイルに直接URLを書いてimportしてもよいのですが、このファイルに使用ライブラリ一覧をまとめておくことで、一括でアップデートができます。
`deno.json`|Deno設定ファイルです。リンターやフォーマッタの設定はこちらに書きます。
`fresh.gen.ts`|Freshの開発サーバーによって自動生成されるファイルです。ルーティングの情報が含まれます。手動で読み書きすることはありません。

また、[404ページの設定方法](https://fresh.deno.dev/docs/concepts/error-pages)や[前ページにヘッダーとフッターを追加する方法](https://fresh.deno.dev/docs/concepts/app-wrapper)、[プラグインシステム](https://fresh.deno.dev/docs/concepts/plugins)などもありますので、公式ドキュメントを参照してみてください。

# deno deployにデプロイ

作成したFreshプロジェクトは、deno deployというホスティングサービスにデプロイすることができます。

deno deployはDeno社が運営しているエッジコンピューティングサービスで、ライトなユースケースなら無料枠の範囲内で使うことができます。Deno製のアプリケーションのデプロイ先としては、最も無難な選択肢となっています。

https://deno.com/deploy

Freshをdeno deployにデプロイするには、GitHub連携が簡単です。

まず、[こちらの記事](https://qiita.com/sugimount-a/items/415116b2fc7ef2bfd74d)などを参考に、FreshプロジェクトをGitHubリポジトリにアップロードします。(GitHubアカウントが無い人は取得してください。)

https://qiita.com/sugimount-a/items/415116b2fc7ef2bfd74d

次に、GitHubアカウントで[deno deployにログイン](https://dash.deno.com/projects)します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/b9138f38-cbd8-5333-4c61-3f553dc04b9b.png)

ログインすると、プロジェクト一覧画面になります。ここで青色の[+New Project]を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/a4836336-d3ba-9bbe-f2d7-7409f7eee738.png)

プロジェクト作成ページでは、[Deploy from GitHub repository]欄に必要項目を入力します。

- Select GitHub repository: 先ほど作成した、デプロイ対象のリポジトリ
- Select Production branch: mainブランチを選択。その後エントリポイントを聞かれるので、`main.ts`を選択。
- Project name: プロジェクト名。デプロイが公開されるドメインは、`<プロジェクト名>.deno.dev`になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/e253a445-5c3b-d067-9aeb-211b91c3d2c1.png)

入力してLinkボタンを押すと、公開完了です。

`https://<プロジェクト名>.deno.dev/`にアクセスして、自分が作ったWebサイトが表示されるか試してみてください。

#### deployctl

なお、GitHubリポジトリを作成しなくても、[deployctl](https://deno.com/deploy/docs/deployctl)というCLIツールからデプロイすることもできます。

プロジェクト作成ページから[Deploy from the command line]を選択して空のプロジェクトを作成したのち、以下のコマンドを使用してデプロイできます。

```shell
# deployctlインストール
> deno install --allow-all --no-check -r -f https://deno.land/x/deploy/deployctl.ts
# ローカルからデプロイ (プロジェクト名は自分で設定したもの)
> deployctl deploy --project=helloworld main.ts
```

#### GitHub連携

(deployctlではなく)GitHub連携でデプロイしている場合、プルリクエストを出すと自動でプレビュー用のブランチが発行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/e971d6d1-4b92-29cf-1172-3be45429afc2.png)

これがめちゃくちゃ便利なので、Github連携をおすすめしています。

# まとめ

Freshを使用してプロジェクト作成から本番環境にデプロイまでやってみました。

Freshでは、Freshを使用して作成されたWebページの例がいくつか公開されています。
これらを活用することで、本格的なWebサイトを製作することが出来そうです。

- [Fresh Charts Example](https://fresh-charts.deno.dev/)：FreshとChart.jsを使用して図表をレンダリングする例
  - GitHubリポジトリ：https://github.com/denoland/fresh_charts

https://fresh-charts.deno.dev/

- [fresh-with-signals](https://fresh-with-signals.deno.dev/)：FreshでPreact Signalsを使用するデモ
  - GitHubリポジトリ：https://github.com/lucacasonato/fresh-with-signals

https://fresh-with-signals.deno.dev/

- [Fresh Components](https://fresh.deno.dev/components)：Freshの便利コンポーネント集
  - GitHubリポジトリ：https://github.com/denoland/fresh/tree/main/www/components
  - ボタンやカルーセルの実装例などが公開されている

https://fresh.deno.dev/components

- [TSX Tabler Icons](https://tabler-icons-tsx.deno.dev/)：[Tabler Icons](https://tabler-icons.io/)のさまざまなアイコンをFreshから使えるようにコンポーネント化したもの
  - GitHubリポジトリ：https://github.com/hashrock/tabler-icons-tsx

https://tabler-icons-tsx.deno.dev/

- [Fresh Showcase](https://fresh.deno.dev/showcase)：Freshで実装されたサイト集
  - GitHubリポジトリ：https://github.com/denoland/fresh/blob/main/www/data/showcase.json

https://fresh.deno.dev/showcase