fresh v1.5がリリースされました。
この記事では主な変更点などについて解説します。
Partials
SPAライクなクライアントサイドでのページ遷移を実現するためにPartialsという機能が導入されました。
基本的な使い方
以下のコードを例に見てみます。
// routes/docs/[id].tsx
import { Partial } from "$fresh/runtime.ts";
export default function Page({ docs, currentDoc }: { docs: Array<Doc>, currentDoc: Doc }) {
return (
<>
<Sidebar docs={docs} />
<Partial name="docs-main-content">
<MainContent doc={currentDoc} />
</Partial>
</>
);
}
function Sidebar({ docs }: { docs: Array<Doc> }) {
return (
<nav f-client-nav>
<ul class="flex flex-col gap-2">
{docs.map((doc) => (
<li key={doc.id} class="shadow-md p-4">
<a href={`/docs/${doc.id}`} class="font-bold">{doc.title}</a>
</li>
))}
</ul>
</nav>
);
}
Partialsを有効化する上で重要なのは以下の点です。
- クライアントサイドナビゲーションを有効化したいリンクを子孫に持つコンテナ要素に
f-client-nav
属性を指定する。<nav f-client-nav> // ... <a href={`/docs/${doc.id}`} class="font-bold">{doc.title}</a> // ... </nav>
- クライアントサイドナビゲーションの実行時に動的に変化して欲しい領域を
<Partial>
でラップする// ... <Partial name="docs-main-content"> <MainContent doc={currentDoc} /> </Partial>
こうすることで、f-client-nav
が指定されたコンテナ要素配下にあるリンクをクリックした際に、ページ全体のリロードが行われずにクライアントサイド側でのページ遷移が行われます。この際に、<Partial>
で囲まれた領域のみが更新されます。
デフォルトでは、freshはf-client-nav
が指定されたコンテナの子孫のリンクがクリックされた際に、href
で指定されたURLに対してfetch()
でリクエストを行います。すると、freshのサーバーが対象ページをSSRした結果をHTMLとして返却するため、その中から<Partial>
で囲まれた領域に該当するHTMLのみを抽出して内容を差し替えます。このようにすることで、ページ全体でのリロードの発生を回避しています。
f-partial
による最適化について
f-client-nav
によるクライアントサイドでのページ遷移の実行時に、freshはfetch()
によってページ全体のSSRを要求します。そのため、場合によっては、多少のオーバーヘッドが生じる可能性も考えられます。
そういったケースでは、f-partial
属性を活用することで最適化が可能です。
function Sidebar({ docs }: { docs: Array<Doc> }) {
return (
<nav f-client-nav>
<ul class="flex flex-col gap-2">
{docs.map((doc) => (
<li key={doc.id} class="shadow-md p-4">
<a
href={`/docs/${doc.id}`}
f-partial={`/partials/docs/${doc.id}`}
class="font-bold"
>
{doc.title}
</a>
</li>
))}
</ul>
</nav>
);
}
例えば、上記のコードでは、<a>
要素にhref
属性とは別にf-partial
属性が設定されています。freshはこのようなリンクがクリックされた際は、href
ではなくf-partial
で指定されたURLに対してfetch()
を実行します。(freshがfetch()
で問い合わせる先のURLが変わるだけで、ページ遷移後のブラウザーのURLについてはhref
で指定された方のURLに更新されます。)
上記のケースでは/partials/docs/${doc.id}
に対して問い合わせが行われるため、routes/partials/docs/[id].tsx
に<Partial>
を返却するルートを定義しておきます。
// routes/partials/docs/[id].tsx
import { Partial } from "$fresh/runtime.ts";
import { defineRoute } from "$fresh/server.ts";
export default defineRoute(async (_, ctx) => {
const doc = await loadDoc(ctx.params.id);
return (
<Partial name="docs-main-content">
<MainContent doc={doc} />
</Partial>
);
});
こうすることでページ全体がSSRされることを回避できるため、パフォーマンスの改善が期待されます。
置き換えモード
<Partial>
コンポーネントには任意でprops.modeを指定できます。
これによって、ページ遷移後の<Partial>
の更新方法をカスタマイズできます。
props.mode | ページ遷移時の挙動 |
---|---|
replace | <Partial> 配下の内容を新しい内容でそのまま置き換える (デフォルト) |
prepend | <Partial> の先頭に新しい内容を挿入する |
append | <Partial> の末尾に新しい内容を挿入する |
data-current
/data-ancestor
属性の導入
<a>
要素に自動でdata-current
/data-ancestor
属性が付与されるようになりました。
属性 | 付与される条件 |
---|---|
data-current | 該当の<a> 要素のhref が現在のページのURLに一致する場合に付与されます。 |
data-ancestor | 該当の<a> 要素のhref が現在のページのURLの祖先ページである場合に付与されます。 (例: 現在のページが/users/123 で該当要素のhref 属性が/users であればdata-ancestor が設定されます) |
スタイルの適用を容易にするために使用されることが想定されているようです。
// Twindでの使用例
<a
class="[data-current]:font-bold"
href={x.link}
>
{x.title}
</a>
事前ビルド
プラグインに実験的なbuildStart
とbuildEnd
フックの追加
プラグインにbuildStart
とbuildEnd
という2種類のフックが追加されました。
⚠️これらのフックはまだ実験的APIという位置づけのため、今後、変更が入る可能性があります。
import type { Plugin } from "$fresh/server.ts";
const samplePlugin: Plugin = {
name: "sample",
buildStart() {
console.info("[build] 事前ビルドを開始します...");
},
buildEnd() {
console.info("[build] 事前ビルドが完了しました。")
}
};
それぞれdeno task buildによる事前ビルドの実行前後のタイミングで呼ばれます。
これらによって、将来的にプラグイン経由でTailwind CSSやSassなどを利用できるようにすることなどが想定されているようです。
esbuildのメタファイルがサポート
esbuildのメタファイルがサポートされました。
deno task build
を実行すると、_fresh/metafile.json
にメタファイルが保存されます。
Bundle Size Analyzerなどで分析できます。
freshの設定 (fresh.config.ts
)
事前ビルドの出力先ディレクトリのカスタマイズ (build.outDir
)
FreshOptionsにbuild.outDir
オプションが追加されました。
これを指定することで、deno task build
の実行結果を_fresh
以外のディレクトリへ出力できます。
例えば、以下の場合、./dist
にビルド結果が出力されます。
// fresh.config.ts
import { defineConfig } from "$fresh/server.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
export default defineConfig({
plugins: [twindPlugin(twindConfig)],
build: {
outDir: "./dist",
},
});
esbuildのターゲットのカスタマイズ (build.target
)
FreshOptionsにbuild.target
オプションが追加されています。
これによりesbuildのtarget
オプションをカスタマイズできます
// fresh.config.ts
export default defineConfig({
plugins: [twindPlugin(twindConfig)],
build: {
target: ["es2020"],
},
});
無視したいルートファイルのパターンの指定がサポート (router.ignoreFilePattern
)
router.ignoreFilePattern
オプションが追加されました。routes/
配下において無視したいファイルのパターンを指定できます。
デフォルトでは、routes
配下のテストファイル(*.test.ts
など)が無視されるよう設定されています。
// fresh.config.ts
export default defineConfig({
plugins: [twindPlugin(twindConfig)],
router: {
// 例) `*.spec.ts(x)`ファイルを無視する
ignoreFilePattern: /\.spec\.(?:ts|tsx)$/
}
});
サーバーに関する設定のカスタマイズがサポート (FreshOptions.server
)
server
オプションによってfreshの起動ポートなどをカスタマイズできます。
// fresh.config.ts
export default defineConfig({
plugins: [twindPlugin(twindConfig)],
server: {
port: 3000,
},
});
サーバー
オプショナルなダイナミックルートパラメータ
[[name]]
のような形式でオプショナルなダイナミックルートパラメータを定義できるようになりました。
例えば、routes/docs/[[version]]/index.tsx
を用意しておくと、以下のいずれかのパターンにマッチします。
/docs/v1
/docs/
非Islandコンポーネントでのフックの使用について
非IslandコンポーネントでuseState
またはuseReducer
を使用した際に、以下のようなエラーが発生するように改善されました。
Error: Hook "useState" cannot be used outside of an island component.
その他の改善点
デフォルトのエラーページの改善
開発時にエラーが発生した際のデフォルトのエラーページが改善されました。
以下のようにエラーが発生した箇所とその周辺のソースコードが表示されます。
エラーメッセージ
5 | export default function Home() {
6 | const count = useSignal(3);
> 7 | throw new Error("エラーメッセージ");
| ^
8 | return (
9 | <>
10 | <Head>
インラインの<script>
へのnonce
の付与
以下のようなインラインの<script>
にもnonce
が付与されるように改善されました。
<script dangerouslySetInnerHTML={{ __html: "alert('hi')" }} />
<Head>
配下の要素でのkey
のサポートについて
以下のように同一ページで複数回<Head>
が使用された際に、重複したタグが出力される問題を修正することが目的のようです
例えば、以下のようにname
が重複した<meta>
が複数回宣言されていたとします。
<Head>
<meta name="og:title" content="foo" />
</Head>
<Head>
<meta name="og:title" content="bar" />
</Head>
この場合、freshは<head>
タグ配下に以下のような<meta>
タグを生成します。
<meta name="og:title" content="foo" />
<meta name="og:title" content="bar" />
こういった場合、<meta>
にkey
属性を指定することで重複を取り除くことができます。
<Head>
<meta name="og:title" content="foo" key="title" />
</Head>
<Head>
<meta name="og:title" content="bar" key="title" />
</Head>
この場合、<head>
タグ配下には以下のように<meta>
タグが生成されます。
<meta name="og:title" content="bar" />