fresh v1.2がリリースされました。
この記事では主な変更点などについて解説します。
このリリースに合わせて、PreactのメンテナーであるMarvin Hagemeister氏がDeno社に入社されたことが発表されています。これからMarvin Hagemeister氏を中心に、フルタイムでFreshの開発が進められていくことが計画されているようです。
アップデートについて
freshはアップデート用のスクリプト(update.ts)を提供しています。
以下のコマンドを実行すると、v1.2へアップデートすることができます。
$ deno run -A -r https://fresh.deno.dev/update .
また、このバージョン以降からinit.ts (freshのプロジェクト初期化用のスクリプト)で作成したプロジェクトでは、deno task update
でもfreshをアップデートすることができます。
Islandコンポーネントに関する改善
props.children
のサポート
Islandコンポーネントでprops.children
がサポートされました。
import type { PageProps } from "$fresh/server.ts";
import Collapse from "../islands/Collapse.tsx";
function Content() {
return <div>foobar</div>;
}
export default function Index(props: PageProps) {
return (
<Collapse>
<Content />
</Collapse>
);
}
またIslandコンポーネントをネストすることもできます。
export default function Index(props: PageProps) {
return (
<Collapse>
<Collapse>
<Content />
</Collapse>
</Collapse>
);
}
制限として、children
以外のprops
にコンポーネントを渡すことはまだサポートされていません。
props
にシグナル/Uint8Array
/循環したオブジェクトを渡せるように
Preact SignalsのシグナルをIslandコンポーネントのprops
として渡せるようになりました。
import type { PageProps } from "$fresh/server.ts";
import { useSignal } from "@preact/signals";
import Counter from "../islands/Counter.tsx";
import Double from "../islands/Double.tsx";
export default function Index(props: PageProps<string>) {
const count = useSignal(0);
return (
<>
<Counter count={count} />
<Double count={count} />
</>
);
}
また、循環したオブジェクトやUint8Array
もサポートされました。
例えば、以下のように循環したオブジェクトを渡すこともできます。
import Example from "../islands/Example.tsx";
export default function Page() {
const data = { a: 1, b: { c: 2 } };
data.d = data;
return (
<Example data={data} />;
);
}
islands
のサブディレクトリのサポート
今までは、Islandコンポーネントはislands
ディレクトリの直下に配置する必要がありました。(例: islands/Counter.tsx
)
この仕様が拡張されて、islands
のサブディレクトリに配置したIslandコンポーネントもfreshによって認識されるようになりました (例: islands/sub_directory/Counter.tsx
)
npmパッケージのサポート
Islandコンポーネントでnpm:
が利用できるようになりました。
// islands/Example.tsx
import truncate from "npm:lodash.truncate@4.4.2";
interface Props {
text: string;
}
export default function Example({ text }: Props) {
return <span>{ truncate(text) }</span>;
}
注意点として、Deno Deployではまだnpm:
がサポートされていません。
Deno Deployでのnpm:
サポートについては、まもなくサポートされる計画があるようなので、もし気になる場合はDeno DeployのChangelogなどをウォッチしておくとよいかもしれません。
プラグインシステムでrenderAsync
フックがサポート
freshにはプラグインシステムがあります。
これはfreshのライフサイクルにおける様々なタイミングに対してフックを提供することで、freshを拡張できるようにするための仕組みです。
v1.1の時点では、SSRの実行前後のタイミングに対するフック(render
フック)のみがサポートされていました。
このrender
フックを活用することにより、SSRによって生成されたHTMLに基づいてTwindなどのCSSランタイムにスタイルシートを生成させることなどが出来ました。
実際にfreshの公式からこの機能を活用したTwind向けのプラグインが提供されています。
制限として、このrender
フックは同期的に動作することが想定されており、非同期処理を仕込むことができないという課題がありました。
そのため、UnoCSSなどの非同期に動作するCSSエンジンをプラグイン経由でサポートすることができない課題がありました。
この制限を解消するため、renderAsync
という新しいフックが追加されました。
実際にこのrenderAsync
フックを活用してUnoCSSプラグインを実装するPRがfreshのリポジトリに作成されています。
これにより、近い将来、fresh公式からUnoCSSプラグインが提供される可能性もありそうです。
deno.json
サポートの改善
imports
による依存管理がサポート
今まで、freshではimport_map.json
というファイル(Import Maps)で依存関係を管理していました。
また、Deno v1.30のリリースで、deno.json
でImport Mapsが定義できるようになりました。
Deno本体でのこの変更に合わせて、freshでもdeno.json
のimports
で定義されたImport Mapsを解釈してくれるようになりました。
今後、init.tsで新しく作成されたプロジェクトについては、このdeno.json
での依存管理がデフォルトになります。
また、アップデートスクリプトを使用してアップデートした際も、import_map.json
からdeno.json
へ自動で移行されます。
祖先ディレクトリからのdeno.json
の探索がサポート
今まで、freshではカレントディレクトリ配下にdeno.json
が存在することが想定されていました。
このリリースから、カレントディレクトリの祖先からもdeno.json
が探索されるようになりました。
これは、freshのプロジェクトをサブディレクトリに作成しているようなケースで活用されることを想定しているようです。
例えば、以下は./web
にfreshのプロジェクトが作成されていますが、このような場合、freshは./deno.json
を読み込んでくれます。
.
├── deno.json
├── web
│ ├── routes
│ │ ├── index.ts
│ │ └── about.ts
│ └── fresh.gen.ts
└── README.md
HEADメソッドのカスタムハンドラーが定義できるように
freshはSSR実行時の挙動を調整したり、REST APIなどの実装をサポートすることなどを目的として、カスタムハンドラーという機能を提供しています。
このカスタムハンドラーでHEADメソッドを処理できるようになりました。
HEADメソッドがリクエストされた際に、handler.HEAD
が定義されていればそれが呼ばれます。
もしhandler.HEAD
が未定義であれば、そのHEADリクエストは代わりにhandler.GET
で処理されます。
// routes/api/example.ts
import type { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
HEAD() {
return new Response(null, {
headers: {
"Content-Type": "text/plain;charset=UTF-8",
"Content-Length": "13",
},
});
},
GET() {
return new Response("Hello, world!", {
headers: {
"Content-Type": "text/plain;charset=UTF-8",
"Content-Length": "13",
},
});
},
};
createHandler()
の追加
新しくcreateHandler()というAPIが追加されました。
主にテストでの使用が想定されているようですが、それ以外の用途でも活用できそうです。
// test.ts
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.190.0/testing/asserts.ts";
import manifest from "./fresh.gen.ts";
import { createHandler } from "$fresh/server.ts";
import twindv1 from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts";
Deno.test("handlers", async (t) => {
const handler = await createHandler(manifest, { plugins: [twindv1(twindConfig)] });
await t.step("GET /", async () => {
const res = await handler(new Request("http://localhost:8000/"), {
localAddr: {
hostname: "127.0.0.1",
port: 8000,
transport: "tcp",
},
remoteAddr: {
hostname: "127.0.0.1",
port: 50000,
transport: "tcp",
}
});
assertEquals(res.status, 200);
assertStringIncludes(await res.text(), "Hello world!");
});
});
ページのレンダリング時のステータスコードやレスポンスヘッダーのカスタマイズ
HandlerContext.renderがoptions
引数をサポートしました。
以下のオプションがサポートされているようです。
オプション | 説明 |
---|---|
status | HTTPステータスコードを上書きできます。 |
statusText | HTTPステータスメッセージを上書きできます。 |
headers | HTTPヘッダを上書きできます。 |
これにより、例えば、ページをレンダリングする際のレスポンスのステータスやヘッダーをカスタマイズできます。
import type { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(req, ctx) {
return ctx.render(null, {
// カスタムヘッダーを設定
headers: { "x-custom-header": "foo" },
});
},
};
export default function Index(props: PageProps) {
...
}
_app.tsx
でパスパラメータなどを参照できるように
アプリケーションラッパーコンポーネント(_app.tsx
)のprops
の型であるAppPropsがPagePropsの内容も受け取るように拡張されました。
これにより、パスパラメータなどを取り扱うことができるようになりました。
// routes/_app.tsx
export default function App(props: AppProps) {
const { name } = props.params;
...
}
起動ポートに関する改善
PORT
環境変数によって起動ポートを変更できるようになりました。
$ PORT=3000 deno task start
...
🍋 Fresh ready
Local: http://localhost:3000/
それ以外にも、PORT
環境変数やport
オプションによってポートが指定されなかった際に、8000〜8020までの中から空いているポートが自動で選択されるように改善が行われています。
パフォーマンス改善
TBTやTTIの改善のため、Islandコンポーネントのレンダリングがscheduler.postTaskを使用して実行されるように挙動が変更されました。
アプリケーションにおけるプラグインやIslandコンポーネントの数が増えれば増える程、TTIが伸びる問題を解消したいのが背景にあるようです。
もしpostTask
が利用できない場合は、setTimeout
にフォールバックされます。
それ以外にも、Freshの各種エントリポイントやIslandコンポーネントなどがlink[rel=modulepreload]
によって事前読み込みされるように改善されました。
その他の改善
SSR時の問題を見つけやすくするため、サーバサイドでもpreact/debug
が読み込まれるようになりました。