fresh v1.4がリリースされました。
この記事では主な変更点などについて解説します。
事前ビルドのサポート
Islandコンポーネントなどの事前ビルドがサポートされました。
deno task build
を実行すると、_fresh
ディレクトリにIslandコンポーネントなどをesbuildによってバンドルした結果が出力されます。(この_fresh
ディレクトリは.gitignore
に含めることが推奨されます。)
# 1. ビルドを実行
$ deno task build
# 2. _freshディレクトリが作成されます
$ cat _fresh/snapshot.json
サーバの起動時にfreshは自動で_fresh
ディレクトリを探索し、見つかればそこに格納されたバンドルを利用してくれます。これによりコールドスタート時間の短縮が期待されます。
$ deno run -A main.ts
Using snapshot found at /path/to/fresh-project/_fresh
この事前ビルド機能はオプトイン方式によるものであり、従来どおり、ビルドステップなしでの開発やデプロイは引き続きサポートされています。ローカルでは従来どおりの方法で開発をし、本番環境にデプロイするときだけ事前ビルドを行うことも可能です。
移行について (fresh.config.ts
)
※fresh v1.4以降、新規に作成したプロジェクトにおいてはこの作業は不要です。
この修正の影響により、dev.ts
で使用することが想定されているdev()に変更が入っており、移行が必要になる場合がありそうです。
具体的には、まずfresh.config.ts
を用意します。
import { defineConfig } from "$fresh/server.ts";
import twindv1 from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts";
export default defineConfig({
plugins: [twindv1(twindConfig)]
});
次に、main.ts
をfresh.config.ts
からfreshに関する設定を読み込むように変更します。
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
+import config from "./fresh.config.ts";
-import twindv1 from "$fresh/plugins/twindv1.ts";
-import twindConfig from "./twind.config.ts";
-
-await start(manifest, { plugins: [twindv1(twindConfig)] });
+await start(manifest, config);
同様に、dev.ts
に関してもfresh.config.ts
から設定を読み込み、dev()
関数に渡すように修正します。
import dev from "$fresh/dev.ts";
+import config from "./fresh.config.ts";
-await dev(import.meta.url, "./main.ts");
+await dev(import.meta.url, "./main.ts", config);
レイアウト
freshではroutes/_app.tsx
を用意することで、各Routeに共通のレイアウトを定義することができました。
しかし、このファイルはアプリケーションごとに一つしか用意できません。
この課題を解消するため、レイアウトという機能が実装されました。この機能を活用することで、routes/_app.tsx
と同様に、特定のRouteに対して共通のレイアウトを定義することができます。
レイアウトを使いたい際は、_layout.tsx
という名前のファイルをroutes
配下の任意のディレクトリに配置します。これにより、対象ディレクトリ及びその子孫のディレクトリの各Routeがレンダリングされる際に、対象のレイアウトが自動で適用されます。
// routes/admin/_layout.tsx
import type { LayoutProps } from "$fresh/server.ts";
export default function AdminLayout({ Component }: LayoutProps) {
return (
<section>
<h2>Admin</h2>
<div>
<Component />
</div>
</section>
);
}
また、レイアウトは入れ子にすることも可能です。
例えば以下のようなディレクトリ構造があったとします。
routes
├── _app.tsx
├── _layout.tsx
├── admin
│ ├── _layout.tsx
│ └── index.tsx
└── index.tsx
この場合、routes/admin/index.tsx
には以下のレイアウトが適用されます。
- (1)
routes/_app.tsx
- (2)
routes/_layout.tsx
- (3)
routes/admin/_layout.tsx
また、レイアウトには後述するdefine
ヘルパーが提供されているため、こちらを使用して定義することも可能です。
import { defineLayout } from "$fresh/server.ts";
export default defineLayout((_req, { Component }) => {
return (
<section>
<h2>Dashboard</h2>
<main>
<Component />
</main>
</section>
);
});
このレイアウト機能のサポートに合わせて、RouteConfigにskipAppWrapper
/skipInheritedLayouts
オプションが追加されています。
これらのオプションによって、特定のRouteにおいてレイアウトに関する挙動を変更することができます。
オプション | 説明 | デフォルト |
---|---|---|
skipAppWrapper | true を指定すると、対象Routeに対するroutes/_app の適用が無効化されます | false |
skipInheritedLayouts | true を指定すると、祖先のディレクトリからのレイアウトの継承が無効化されます | false |
また、レイアウトはv1.3で実装された非同期Routeコンポーネントとして実装することも可能です。この場合、defineLayout
ヘルパーを使用すると便利です。
import { defineLayout } from "$fresh/server.ts";
export default defineLayout<AppState>(async (_req, { Component, params, state }) => {
const user = await ctx.state.db.findUser(params.id);
return (
<section>
<h2>Hi {user.name}!</h2>
<main>
<Component />
</main>
</section>
);
});
app wrapper
html
, head
, meta
などのタグのサポート
routes/_app.tsx
からhtml
, head
, meta
などのタグをレンダリングできるようになりました。
これにより、html
のlang
属性の設定などが容易になりそうです。
// routes/_app.tsx
import { AppProps } from "$fresh/server.ts";
import Header from "../components/Header.tsx";
import Footer from "../components/Footer.tsx";
export default function App({ Component }: AppProps) {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sample Fresh Project</title>
</head>
<body>
<Header />
<main>
<Component />
</main>
<Footer />
</body>
</html>
);
}
非同期Routeコンポーネント形式のサポート
routes/_app.tsx
をv1.3でサポートされた非同期Routeコンポーネントとして定義できるよう改善が行われています。この場合、defineApp
を使用すると便利です。
// routes/_app.tsx
import { defineApp } from "$fresh/server.ts";
export default defineApp(async (_req, { Component }) => {
const title = await getTitle();
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<Component />
</body>
</html>
);
});
define*
ヘルパー
freshの各種構成要素の定義を簡易化するためのヘルパーが追加されました。
すでに登場しているdefineConfig
やdefineLayout
, defineApp
に加えて、defineRouteというヘルパーが追加されています。
// routes/blog/[id].tsx
import { defineRoute } from "$fresh/server.ts";
export default defineRoute(async (req, ctx) => {
const content = await readContent(ctx.params.id);
return (
<div>{content}</div>
);
});
Route Groups
freshでNext.jsライクなRoute Groups機能がサポートされました。
Route Groupを定義したい場合、routes/
ディレクトリに(<name>)
というような形式のディレクトリを作成します。
このような形式で命名されたディレクトリについては、freshによって特別な扱いがなされます。 例えば、以下のような構成のプロジェクトが存在したとします。
routes
├── (_islands)
│ └── Counter.tsx
├── (dashboard)
│ ├── _layout.tsx
│ ├── _middleware.ts
│ └── account.tsx
├── _app.tsx
├── _layout.tsx
└── index.tsx
この場合、/account
にアクセスすることでroutes/(dashboard)/account.tsx
がレンダリングされます。また、Route Groupごとにレイアウトやミドルウェアを配置することができます。
例外として、(_foo)
というような形式で命名されたディレクトリはfreshによってファイルシステムルーティングの対象から除外されます。
そのため、例えば、(_components)
ディレクトリを用意し、そこに特定のRouteに関連したコンポーネントなどの一覧をまとめておくことなどが可能です。
routes/(dashboard)
├── (_components)
│ └── Chart.tsx
├── (_utils)
│ └── index.tsx
├── _layout.tsx
└── account.tsx
└── index.tsx
その他にも、特殊なルールとして、freshは(_islands)
という名前のディレクトリに配置されたコンポーネントをIslandコンポーネントとして認識します。
その他の改善について
サーバでHTTPSがサポート
start()関数にTLS関連のオプションを指定することで有効化できます。
プラグイン経由でミドルウェアを注入する際の挙動の改善
複数のプラグインから同一Routeに適用されるミドルウェアが複数存在する場合に、それらが適切にマージされるように挙動が改善されました。
文字列形式のイベントハンドラのサポート
文字列形式でイベントハンドラを設定できるようになりました。(型エラーが発生するため@ts-expect-error
の指定などが必要です)
<button
type="button"
// @ts-expect-error
onClick="alert('foobar')">
Hello
</button>