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を有効化する上で重要なのは以下の点です。

  1. クライアントサイドナビゲーションを有効化したいリンクを子孫に持つコンテナ要素にf-client-nav属性を指定する。
      <nav f-client-nav>
        // ...
              <a href={`/docs/${doc.id}`} class="font-bold">{doc.title}</a>
        // ...
      </nav>
    
  2. クライアントサイドナビゲーションの実行時に動的に変化して欲しい領域を<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>

事前ビルド

プラグインに実験的なbuildStartbuildEndフックの追加

プラグインにbuildStartbuildEndという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)

FreshOptionsbuild.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)

FreshOptionsbuild.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" />

参考