12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-13

FreshはDeno社が作成しているWebフレームワークです。

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

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

Freshプロジェクトの作成

Denoのインストール

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

Denoをインストール
# mac/linuxの場合
> curl -fsSL https://deno.land/x/install/install.sh | sh
# windowsの場合
> irm https://deno.land/install.ps1 | iex
Denoを最新版にアップデート
> deno upgrade

Freshプロジェクト作成

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

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

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

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

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

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

# 指定したディレクトリに既にファイルが存在する。上書きしてもよいか?
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などで当該ディレクトリに移動したのち、以下のコマンドを打つとサーバーが起動します。

サーバー起動
> deno task start

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

image.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のように、角括弧で囲まれたパスはワイルドカードとして使用されます。

[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関数を作成してエクスポートすることで対応可能です。

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以外のレスポンスを返すことができます。

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を設定することで、ファイルシステムベースではない特殊なルーティングを行うことができます。

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アーキテクチャを採用しています。

islandsアーキテクチャとは?

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

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

image.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されるファイルに記述しなければなりません。

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を使用してJSXをレンダリングします。

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

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

これらの使い方は、詳しくはPreactのドキュメントを参照してください。

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

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

こちらについても、詳しくはPreactのドキュメントFreshのドキュメントを参照してください。

IS_BROWSER変数

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

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}という属性を付与するとよいです。

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することができます。

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

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

twindによるスタイリング

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

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を使用する方法 をどうぞ。

静的ファイルの配信

画像などの静的ファイルは、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ページの設定方法前ページにヘッダーとフッターを追加する方法プラグインシステムなどもありますので、公式ドキュメントを参照してみてください。

deno deployにデプロイ

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

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

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

まず、こちらの記事などを参考に、FreshプロジェクトをGitHubリポジトリにアップロードします。(GitHubアカウントが無い人は取得してください。)

次に、GitHubアカウントでdeno deployにログインします。

image.png

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

image.png

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

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

image.png

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

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

deployctl

なお、GitHubリポジトリを作成しなくても、deployctlというCLIツールからデプロイすることもできます。

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

# 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

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

まとめ

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

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

12
9
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
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?