FreshはDeno社が作成しているWebフレームワークです。
FreshはDenoでフロントエンドを作成する際の選択肢として一番最初に挙がるフレームワークだと思います。登場からもうすぐ1年経つところで、色々改善も入り安定して使えるようになってきたようです。
この記事では、DenoとFreshを使用して、プロジェクト作成から本番環境へのデプロイまでの流れを紹介したいと思います。
Freshプロジェクトの作成
Denoのインストール
Freshは最新版のDenoで動作します。まずDenoをインストールする必要があります。
# mac/linuxの場合
> curl -fsSL https://deno.land/x/install/install.sh | sh
# windowsの場合
> irm https://deno.land/install.ps1 | iex
> deno upgrade
Freshプロジェクト作成
Freshプロジェクトは以下のコマンドで作成できます。
# my-appディレクトリを作成し、その中にFreshプロジェクトを作成
> deno run -A -r https://fresh.deno.dev my-app
なお、ディレクトリを切らずに、カレントディレクトリを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 に移動すると、以下のような画面になっているはずです。
ファイルを変更すると、サーバーは自動で再起動します。またホットリロードにも対応しているため、ブラウザ側でも自動で再読み込みされます。
サーバーを停止するにはターミナルで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
のように、角括弧で囲まれたパスはワイルドカードとして使用されます。
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
を設定することで、ファイルシステムベースではない特殊なルーティングを行うことができます。
// 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アーキテクチャと呼びます。
▲Webページの一部分のコンポーネント(=islands)のみ、JavaScriptを使用してインタラクティブにする。それ以外の部分は完全に静的なHTMLになる。
Freshでは、islands/
ディレクトリに置いたコンポーネントがislandsとして扱われます。
islands/
ディレクトリのコンポーネントは、まずサーバー側でSSRされ、次にブラウザ側でもう一度レンダリングされます(=hydration)。
こうすることで、初期表示ではSSRによって高速に表示しつつ、JSの読み込みが完了したらイベントハンドラなどが流し込まれるようになっています。
逆に言うと、ブラウザ側で実行すべきonClick
などが含まれるコードは、islands/
ディレクトリか、islands/
内からimportされるファイルに記述しなければなりません。
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}
という属性を付与するとよいです。
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することができます。
export function Header() {
return <header>Hello!</header>
}
このディレクトリに置いたコンポーネントは、routes/
からもislands/
からもimportすることができます。必要に応じてIS_BROWSER
による条件分岐をしましょう。
twindによるスタイリング
Freshではtwindプラグインが用意されており、Tailwind CSSを利用したスタイリングが可能になっています。
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/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にログインします。
ログインすると、プロジェクト一覧画面になります。ここで青色の[+New Project]を選択します。
プロジェクト作成ページでは、[Deploy from GitHub repository]欄に必要項目を入力します。
- Select GitHub repository: 先ほど作成した、デプロイ対象のリポジトリ
- Select Production branch: mainブランチを選択。その後エントリポイントを聞かれるので、
main.ts
を選択。 - Project name: プロジェクト名。デプロイが公開されるドメインは、
<プロジェクト名>.deno.dev
になる。
入力して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連携でデプロイしている場合、プルリクエストを出すと自動でプレビュー用のブランチが発行されます。
これがめちゃくちゃ便利なので、Github連携をおすすめしています。
まとめ
Freshを使用してプロジェクト作成から本番環境にデプロイまでやってみました。
Freshでは、Freshを使用して作成されたWebページの例がいくつか公開されています。
これらを活用することで、本格的なWebサイトを製作することが出来そうです。
-
Fresh Charts Example:FreshとChart.jsを使用して図表をレンダリングする例
- GitHubリポジトリ:https://github.com/denoland/fresh_charts
- fresh-with-signals:FreshでPreact Signalsを使用するデモ
-
Fresh Components:Freshの便利コンポーネント集
- GitHubリポジトリ:https://github.com/denoland/fresh/tree/main/www/components
- ボタンやカルーセルの実装例などが公開されている
-
TSX Tabler Icons:Tabler IconsのさまざまなアイコンをFreshから使えるようにコンポーネント化したもの
- GitHubリポジトリ:https://github.com/hashrock/tabler-icons-tsx
- Fresh Showcase:Freshで実装されたサイト集