Fresh Pluginの勉強
ここを見ながら一つ作ってみよう
とらラボさんのlogger実装デモ
他、見つけたプラグイン
Fresh 1.3より pluginによってroutesとmiddlewareを生やすことができるようになった。
まずはBASIC認証するmiddlewareを書いてみる。
ここにあるCustom Handlerの例をmiddlewareに書き換える。
// routes/greet/_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
const USER = "alice"
const PASSWORD = "secret"
export async function handler(
req: Request,
ctx: MiddlewareHandlerContext,
) {
if (req.headers.get("Authorization") !== `Basic ${btoa(`${USER}:${PASSWORD}`)}`) {
const headers = new Headers({
"WWW-Authenticate": 'Basic realm="Fake Realm"',
});
return new Response("Unauthorized", { status: 401, headers });
}
return await ctx.next();
}
これで、greet以下がBASIC認証で保護された。
見様見真似でプラグインにできた。
// plugins/basic.ts
import { MiddlewareHandlerContext, Plugin } from "$fresh/server.ts";
export default function basicAuthPlugin(path: string): Plugin {
const USER = Deno.env.get("BASIC_AUTH_USER");
const PASSWORD = Deno.env.get("BASIC_AUTH_PASSWORD");
const REALM = Deno.env.get("BASIC_AUTH_REALM");
if (!USER || !PASSWORD) {
throw new Error("BASIC_AUTH_USER and BASIC_AUTH_PASSWORD must be set");
}
async function handler(
req: Request,
ctx: MiddlewareHandlerContext,
) {
if (
req.headers.get("Authorization") !==
`Basic ${btoa(`${USER}:${PASSWORD}`)}`
) {
const headers = new Headers({
"WWW-Authenticate": `Basic realm="${REALM || "Fake Realm"}"`,
});
return new Response("Unauthorized", { status: 401, headers });
}
return await ctx.next();
}
return {
name: "basicAuthPlugin",
middlewares: [
{
middleware: {
handler,
},
path,
},
],
};
}
呼ぶときはこんな感じ。
// main.ts
import basicAuthPlugin from "./plugins/basic.ts";
await start(manifest, {
plugins: [
basicAuthPlugin("/greet/"),
twindPlugin(twindConfig),
],
});
// .env
BASIC_AUTH_USER=alice
BASIC_AUTH_PASSWORD=secret
BASIC_AUTH_REALM=MyRealm
- middlewareに渡すパスは先頭と末尾にスラッシュ必須っぽい
- 型の設定ちょっと面倒なのでinitスクリプトあってもいいのかもしれない
無事publishもできたので、こういう感じで使えるようになりましたっと。
import basicAuthPlugin from "https://deno.land/x/hashrock_fresh_plugins@1.0.0/basic.ts";
await start(manifest, {
plugins: [
basicAuthPlugin("/greet/"),
twindPlugin(twindConfig),
],
});
次。pluginからrouteを生やす。
とは言ってもカスタムハンドラあんまり詳しくないのでblogのpluginを見ながら色々試行錯誤。
KVビューアを作りたかったが、とりあえずなにか表示するところまで。
// plugns/kv.ts
import {
Plugin,
} from "$fresh/server.ts";
import Blog from "../routes/blog.tsx";
export default function kvViewerPlugin(): Plugin {
return {
name: "kvViewerPlugin",
routes: [
{
path: "/kv",
component: Blog,
async handler(_req, ctx) {
return await ctx.render("Hello");
},
},
],
};
}
こんな感じでroutesからRouteコンポーネントを取ってきて、データを渡してRenderできることがわかった。
ここまで来たら、ほとんどできないことはないんじゃないかな…?
islandsのみ、ファイルの実体が islands
ディレクトリ下にあることで fresh.gen.tsの生成を行っているはずだから、pluginからは干渉できないはず。
となると、例えばAdminページをpluginから差し込むような場合には、動的なコンポーネントを画面内で使うのは難しいのかもしれない。レガシーなWebアプリみたいに、CRUD生やしてformから登録削除する、みたいなところまでは行けそう。
認証プラグインは普通に作れるのでは。
pluginからislandsを追加することについて。
blog pluginを作ったdeerさんがまずislandsを追加できるIssueとPRを作成。
しかし、islands等を個別対応するよりは、プロジェクト全体をPluginとして扱えるようにしよっか、という包括的な提案がなされる。
細かい議論にはついていけてないけど、なんか良さそうですね。
とはいえislands以外のことはだいたいできることに変わりはないので、最小限の機能を持つプラグインをもっとぽこぽこ作ってみんなをハッピーにしようみたいなのはどんどんやって良さそう。
Basic認証プラグインについてはここに置いた。
さて、最新版でPluginからislandsを作れるようになった(deerさんのPR)
これを試してみたい。予想が正しければ、プラグインからインタラクティブなUIを生やすことが可能になる。
まずはfreshをmainから取ってくるようにする。
"imports": {
"$fresh/": "https://raw.githubusercontent.com/denoland/fresh/81e71adad60c0fa83bc430677d2910785d0184fc/",
ドキュメントはここ。
ここからexampleがリンクされている。
こんな感じでislandsのパスを指定している。
islands: {
baseLocation: import.meta.url,
paths: ["./sample_islands/IslandFromPlugin.tsx"],
},
baseLocationはなんのために必要なんだろう?
あと違うドメインに置いてたりしても取ってこれるのかな?
baseLocationとpathはjoinされる(配列で渡すから短く書くためっぽいな)。
islandsは動的インポートで取ってくるみたい。こういうのってキャッシュされるんだっけか?
そもそもRoute componentもリモートから取ってこれるのか。
import {
Plugin,
} from "$fresh/server.ts";
// import { Hello } from "../components/Hello.tsx";
import Home from "https://raw.githubusercontent.com/hashrock/fresh-badminton-site/main/routes/member.tsx"
export default function helloPlugin(): Plugin {
return {
name: "helloPlugin",
routes: [
{
path: "/hello",
component: Home,
async handler(_req, ctx) {
return await ctx.render("Hello");
},
},
],
};
}
あー、動くなこれ。ただ未キャッシュの状態だと起動が遅くなったりするのかもしれん(fetchが挟まりそう)
ただし、import mapsは手元のが使われるので、外部URLからのインポートを前提としたコンポーネントはimportを相対パスで記述する必要がある($fresh/server.ts
に関してはローカルのdeno.jsonと定義が一緒なので動いたっぽい)
動いたは動いたなぁ。
// plugins/hello.ts
import {
Plugin,
} from "$fresh/server.ts";
import { Hello } from "../components/Hello.tsx";
export default function helloPlugin(): Plugin {
return {
name: "helloPlugin",
routes: [
{
path: "/hello",
component: Hello,
async handler(_req, ctx) {
return await ctx.render("Hello");
}
},
],
islands: {
baseLocation: "https://raw.githubusercontent.com/hashrock/fresh-components-beta/main/components/Map.tsx",
paths: [
"Map.tsx",
]
}
};
}
// components/Hello.tsx
import { JSX } from "preact";
import Map from "https://raw.githubusercontent.com/hashrock/fresh-components-beta/main/components/Map.tsx"
export function Hello(props: JSX.HTMLAttributes<HTMLDivElement>) {
return (
<div {...props}>
<h1>Hello Component</h1>
<Map current="" />
</div>
);
}
この使い方であってるのか…?
気になる点。
baseLocationは何なんだろう?サンプルだとimport.meta.urlを指定することになってるんだが、pluginは同じフォルダ内にコピーして使うことを想定してるのかな?
ここでdirnameを取っているので、ディレクトリを指定してしまうとその親ディレクトリが取れてしまう(ので、前述の利用方法だとわざわざ Map.tsx
まで指定している)
とりあえず外部のroutesやislandsを特別な設定なしに利用できるようになったんだけど推奨する構成がよくわからない。ローカルだけで使うようなツールをinjectするツールなら特に心配なく使っていけるかな
とりあえずpluginのislands追加実装は次バージョンに乗ると思うので、一応pathの件をissueを立てて聞いてみる。