ricomintea’s diary

好きなものを好きでいる

Go+WebAssemblyでfetchした結果を返す

Node.js 20.5.0, Remix 1.19.1

色々やる前にNode.jsのバージョンが18系だったせいで、Goが用意している wasm_exec.js で使われている crypto がないよと言われて動かなかった。気づいた時に泣いた。

u

入力されたURLからHTMLを取得して、OGPのデータを抽出するGoのプログラムがあるとする。

type OGP struct {
  // OGPとして返すデータの型定義
}

func getOGP(url: string) (ogp OGP, err error) {
  // net/httpでフェッチしてレスポンスのHTMLからデータを引っこ抜く
  return ogp, err
}

Goの関数をJavaScript側から呼び出せるようにバインドする。

バインドする関数は、JavaScript側から呼び出し可能なインターフェースにする必要があるため、 getOGP をラップした関数を用意する。

import (
  // wasmが動作する環境の機能にアクセスするためのパッケージ
  "syscall/js"
)

// バインドされる関数は第一引数に呼び出したJavaScript側のコンテキスト(this)、
// 第二引数に関数呼び出し時の引数が渡される
func getOGPForWasm(this js.Value, args []js.Value) {}

func main() {
  // JavaScriptのグローバルコンテキスト(globalThis)にogperという名前で値を設定する
  // 今回はogperに対してキーバリューで関数を持つオブジェクトを設定している
  // キーに対して直接関数や数値・文字列を設定することもできる
  js.Global().Set("ogper", js.ValueOf(map[string]any{
      "getOGP": js.FuncOf(getOGPForWasm),
  }))
}

上のコードでバインディング自体は可能だが、呼び出そうとすると Error: Go program has already exited というエラーになる。

こちらの記事の『めんどく3』にあるがmain関数を動かし続けないといけない。

GoのWASMがライブラリではなくアプリケーションであること - 株式会社カブク

func main() {
  c := make(chan struct {})
  js.Global().Set("ogper", js.ValueOf(map[string]any{
      "getOGP": js.FuncOf(getOGPForWasm)
    }))

  // wasmがJavaScriptからのリクエストに応答できるように、
  // チャネルでプログラムを動作させ続ける
  <-c
}

普通に呼び出したいだけなのに生かし続けないといけない。何でこんなことをという感じ。

ここら辺でRustとかで書いてたらなという気持ちになった。

Go で1行でgoroutineデッドロックを起こす方法

公式ドキュメントとかでmain() のことを current goroutine と表現しているってところでなるほどになった。goroutine終わってないよ!止まるな!をしないといけない。うーん

getOGPForWasm の実装をする。

普通にGoを書く感じでいけるかなと思ったらダメだったやつ。

func getOGPForWasm(this js.Value, args []js.Value) string {
    url := "https://github.com/ihch"

  ogp, err := getOGP(url)
  if err != nil {
    panic(err)
  }

  // オブジェクトそのままでは渡せないのでJSONにシリアライズする
  json, err := json.Marshal(ogp)
  if err != nil {
    panic(err)
  }

  return string(json)
}

js package - syscall/js - Go Packages

Invoking the wrapped Go function from JavaScript will pause the event loop and spawn a new goroutine. Other wrapped functions which are triggered during a call from Go to JavaScript get executed on the same goroutine.

As a consequence, if one wrapped function blocks, JavaScript's event loop is blocked until that function returns. Hence, calling any async JavaScript API, which requires the event loop, like fetch (http.Client), will cause an immediate deadlock. Therefore a blocking function should explicitly start a new goroutine.

曰く、JavaScriptからWASMを介してGoの関数を呼ぶと、JavaScript側のメインスレッドのイベントループを止めてgoroutineを動かしてその中で処理が行われる。イベントループを必要とするような非同期で行われる処理、例えばfetch (http.Get)をしようとすると、そこから進まないデッドロックの状態になる。

こちらの記事では、HTTPリクエストを含む処理をさらにgoroutineで囲い、その中でDOMの書き換えまで行っている。

Go 1.18集中連載 net/httpのマイナーチェンジ | フューチャー技術ブログ

DOMを直接書き込むようなことはせずに、データだけ渡してどう使うかはJavaScript側に任せるみたいな使い方を考えていたので、少し違う方法で実装することになった。

Promiseを生成して、JavaScript側の世界で非同期処理の扱いを制御できるようにした。

func getOGPForWasm(this js.Value, args []js.Value) interface{} {
    url := "https://github.com/ihch"

    handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        resolve := args[0]
        reject := args[1]

        go func() {
            ogp, err := Ogper.GetOGP(url)
            if err != nil {
                reject.Invoke(err)
            }

            responseJson, err := json.Marshal(ogp)
            if err != nil {
                reject.Invoke(err)
            }

            resolve.Invoke(string(responseJson))
        }()

        return nil
    })

    return js.Global().Get("Promise").New(handler)
}

この記事を参考にしたような気がする。確か。

Go WebAssemblyでPromiseを使って非同期化してみた | RE:ENGINES

使えるようになって嬉しくはあるものの何でこんなことを(2回目)の気持ちになった。

WebAssemblyにしてRemixで使う

WebAssemblyにコンパイルしてRemixのプロジェクトにコピーする

GOOS=js GOARCH=wasm go build -o ogper.wasm
cp ~/Projects/my-remix-app/app/wasm
# Goで生成したWebAssemblyをJavaScriptで解釈するためのファイル
cp (go env GOROOT)/misc/wasm/wasm_exec.js ~/Projects/my-remix-app/app/wasm

フロントの方はRemixを使って生成した app/routes/index.tsx

import { json, type V2_MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import fs from 'fs';
import path from 'path';

export const meta: V2_MetaFunction = () => {
  return [
    { title: "New Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

const wasmPath = path.resolve(__dirname, '../app/wasm/ogper-goroutine.wasm');

type WasmModuleCache = {
  [path: string]: WebAssembly.Module;
};

const wasmModuleCache: WasmModuleCache = {};

require('../wasm/wasm_exec.js');
const go = new Go();

export const loader = async () => {
  const wasmModule = wasmModuleCache[wasmPath] ? wasmModuleCache[wasmPath] : await WebAssembly.compile(fs.readFileSync(wasmPath));
  const wasmInstance = await WebAssembly.instantiate(wasmModule, go.importObject);

  if (!wasmModuleCache[wasmPath]) {
    wasmModuleCache[wasmPath] = wasmModule;
    go.run(wasmInstance);
  }

  const og = await ogper.getOGP('https://github.com/ihch');

  return json({ og: JSON.parse(og) });
}

export default function Index() {
  const data = useLoaderData<typeof loader>();

  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1>Welcome to Remix</h1>
      <p>{data.og.title}</p>
      <img src={data.og.image} alt={data.og.title} />
    </div>
  );
}

結果

感想

たまたまGoで書いていたコードがあって、Remixのサーバー側で動かすと都合がよさそうと思いやってみたけど、Go + WebAssembly特有っぽい部分で微妙な部分を感じた。

  • WebAssemblyを wasm_exec.js を使って読み込むとglobalThisにものが入る
    • 気合で wasm_exec.js を書き換えたらなんとかなるのか?えー
  • 他の言語でもそうなのか分からないけど素でフェッチ処理をするとデッドロックが起きるところ

もし今後WebAssemblyにする前提で何かを書くならRustを練習してやるかなーと思った。

リポジトリ

github.com

github.com

資料

js package - syscall/js - Go Packages

this - JavaScript | MDN

イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理

並行モデルとイベントループ - JavaScript | MDN

GoのWASMがライブラリではなくアプリケーションであること - 株式会社カブク

Go で1行でgoroutineデッドロックを起こす方法

Goで作ったロジックにWebUIをつけてGitHubページに公開する | フューチャー技術ブログ

Go 1.18集中連載 net/httpのマイナーチェンジ | フューチャー技術ブログ

Go WebAssemblyでPromiseを使って非同期化してみた | RE:ENGINES

Markdownでやる正規表現

MarkdownをHTMLに変換するものを自作したときに書いたものを振り返りがてら、この記事ではMarkdownの見出しを読み取りHTMLにするところを書こうと思います。

github.com

いくつかの方針

JavaScriptのランタイムのひとつであるDenoを使うことにしました。プログラミング言語の中でここ最近特に慣れがあるJavaScript / TypeScriptを使いつつ、LintとFormatツールを標準で備えているところに嬉しさがあります。

パーサー実装に慣れているわけではなかったので、テスト駆動のように簡単な例から実装していく進め方を取りました。

o

まずは受け取った文字列をそのまま返す状態を作ります。

const markdown = `Text`;

function convertMarkdownToHTML(markdown: string): string {
  return markdown;
}

if (import.meta.main) {
  const html = convertMarkdownToHTML(markdown);
}

見出しを処理する方法を考えます。

const markdown = `# Heading`;

function convertMarkdownToHTML(markdown: string): string {
  return markdown;
}

if (import.meta.main) {
  const html = convertMarkdownToHTML(markdown);
}

Markdownの見出しの構造は、見出しのレベルを表す # の1つ以上の繰り返しと、見出しとして表示する部分(コード中でいう Heading )に分かれています。

このパターンを読み取る方法として正規表現が使えます。

JavaScriptでは正規表現を扱うのに、正規表現リテラルを使う方法と RexExp オブジェクトを使う方法が存在します。

// 正規表現リテラルの定義はスラッシュ2つの間にパターンを書くことでできます。
const regexp1 = /ここに正規表現パターンを記述/;

// RegExpオブジェクトを使う場合はインスタンスを生成します。
const regexp2 = new RegExp('ここに正規表現パターンを記述');

developer.mozilla.org

正規表現のパターンを考えていきますが、エディタの拡張機能やWebアプリケーションで確認しながらやると間違っていないか気づきやすくいいと思います。

www-creators.com

見出しの定義は必ず行の一番最初に来るので正規表現アサーション ^ が使えます。

const heading_regexp = /^/;

見出しのレベルを表す # の1つ以上の繰り返しにマッチするパターンを作ります。

const heading_regexp = /^#+/;

GitHub Flavored Markdownでは見出しレベルとして扱うのは6までとされているので、そこに倣うのなら次のようになります。(今回は別にGitHub Flavored Markdownに従ったわけではなく、ふんわりとやっています。)

const heading_regexp = /^#{1,6}/;

github.github.com

次に見出しとして表示する文字列の部分のパターンを考えます。 # の繰り返しから空白を挟んで任意の文字列にマッチさせたいです。

空白に一致するには \s を使い、任意の文字には . が使えます。

const heading_reg = /^#{1,6}\s+.+/;

Stringオブジェクトの持つ match メソッドに定義した正規表現を渡して使うことができます。

'# heading'.match(heading_reg);
// => ['# heading', index: 0, input: '# heading', groups: undefined]

パターンにマッチして結果が返ってきます。ただこの状態ではせっかくマッチさせた情報が扱いづらいです。一部分の正規表現パターンに対して名前を付けて扱うことができる、名前付きキャプチャグループが使えます。

const heading_reg = /^(?<level>#{1,6})\s+(?<content>.+)/
'# heading'.match(heading_reg);
// => ['# heading', '#', 'heading', index: 0, input: '# heading', groups: {…}]
/*
    0: "# heading"
    1: "#"
    2: "heading"
    groups: {level: '#', content: 'heading'}
    index: 0
    input: "# heading"
*/

developer.mozilla.org

作った正規表現を使って、見出しを一回だけ評価してHTMLに変換する処理を書いてみます。

const markdown = `# Heading`;
const heading_reg = /^(?<level>#{1,6})\s+(?<content>.+)/

type Token = { level: number; content: string; };

function parse(markdown: string): { level: number, content: Token } {
  const heading = markdown.match(heading_reg);
  if (heading) {
    return {
      level: (heading.groups?.level || '#').length,
      content: heading.groups?.content || '',
    },
  }
}

function renderHTML(t: Token): string {
  return `<h${t.level}>${t.content}</h${t.level}>`;
}

function convertMarkdownToHTML(markdown: string): string {
  const parsed = parse(markdown);
  const html = renderHTML(parsed);
  return html;
}

if (import.meta.main) {
  const html = convertMarkdownToHTML(markdown);
  console.log(html);
  // => <h1>Heading</h1>
}

Denoで(または型情報を落とせばでブラウザのコンソール上でも)実行すると、 h1タグとなって見出しがログに出力されます。

ここから立派なMarkdown to HTMLの変換器を作るために、他の構文をサポートする、parse関数で1文字ずつ順繰り見る、render関数で気合いジェネレーターを書くなどの工程が必要です。いつか記事になることを夢にも見ています。

「マークダウンパーサー作ろう」という気持ちにさせてくれ、パース部分で参考にさせていただいた記事です。感謝 :pray:

www.m3tech.blog

web speed hackathon2023で遊んでいる

全然関係ないけど何かを書くときにWYSIWYGとしてnotionがすごい使いやすい。
気軽さ重視で notion は公開用に使いたくないんだけど、はてなに移すときにファイル周りが面倒で難しい。

リンク貼る。

tested-talos-429.notion.site

基本的にはLighthouseで最大の問題になっているところから潰していく方針で進めた。

主にやったこと

やったときに参考にした資料はNotionの方に作業内容と一緒に記載している。

  • 画像の圧縮
    • JPEGから圧縮率のいいWebPに変換する
    • メインビジュアル用とその他小さく表示する用に複数の画質で非可逆圧縮をかける
    • 巨大ロゴSVGを圧縮
  • JavaScriptバンドルサイズの削減
    • productionビルドを指定
    • bundle analyzerの導入
    • ビルドターゲットを見直して必要ないポリフィルの削除
    • クライアントで処理している大きなデータ(zipcode-ja)をAPIに委譲
    • (react-iconなどを)Treeshakeの効く形でimportする
    • バンドル時にterserとgzipプラグインを差し込む
  • FCS向けの対応
    • ファーストビュー以外の画像を遅延読み込みさせる
  • よく分からない重たい処理を簡単にする
  • サーバー側
    • キャッシュ制御の指定
    • gzipでレスポンスを圧縮する
      • 画像やGraphQLレスポンス、JavaScriptサイズに効く

できそうなこと

  • 動画ファイルの圧縮
  • GraphQLでレスポンスに1秒かかってるところ多分N+1
  • recoil外す
  • apollo-clientの圧縮(treeshaking)がビルド設定の問題かうまくいってない

Web Worker面白い

Web Workerは開いているページと同じプロセス内の別スレッドで処理をする仕組みです。

developer.mozilla.org

ワーカーはDOM APIなどのいくつかのブラウザの機能にアクセスすることができませんが、メインとなるページのレンダリングや、ユーザー操作に影響を与えるような重たい処理を別のスレッドに分離することができます。

Web WorkerとPromise

Web Workerは別のスレッドを用意してそこで実行されますが、Promiseは基本的にメインスレッド内で実行されます。Promiseの非同期処理はひとつのスレッド内で、時間的に処理を分割して処理を進めるようになっています。 await は分かりやすくPromiseの処理を待ちます。

jsprimer.net

よく並列処理と並行処理の違いとされるものです。

ワーカーの種類

ワーカーはそのアクセス範囲や方法でいくつかの種類が存在します。

専用ワーカー dedigated worker

専用ワーカーはいわゆる普通のワーカーで、ひとつのタブプロセスからのみ呼び出されます。

共有ワーカー shared worker

共有ワーカーは、同一ドメインのタブプロセスで共有されるワーカーです。

サービスワーカー service worker

サービスワーカーは、あるドメインとパスに紐づけられたワーカーで、リクエストに対して手を加えたり、キャッシュを返却したりするプロキシサーバーの様な役割ができます。

動作しているワーカーの確認

Chromeでの確認方法です。他のブラウザでも確認する手段はあると思いますが特に調べていません。

専用ワーカーは確認したいページ中でデベロッパーツールを開き、 Console タブのプルダウン内に表示されます。

動作している shared workerは chrome://inspect/#workers から、service workerは chrome://inspect/#service-workers から確認することができます。

ワーカー情報の下に表示されている inspect をクリックするとデベロッパーツールが開かれます。

最近見たワーカーを使った面白テーマ

OffscreenCanvas

まだ実験的機能の段階ですが、OffscreenCanvasというWeb APIがあります。メインスレッドであるページでCanvasを取得し、Web Workerの呼び出し時に渡すことでDOM APIにアクセスできないWeb Workerから画面描画を可能にします。

developer.mozilla.org

記事だとこれが分かりやすそう。

ics.media

Workerから描画をすることで、ユーザーの操作性を邪魔せずにリッチなアニメーションなどの見た目が実現できます。見た目はリッチだけどこのサイト重いなみたいなのを回避できる。

WebAssemblyの応用

またWebAssemblyの登場で、ネイティブ向けに実装されていたJavaScript以外の言語のリソースがブラウザ上で扱えるようになりました。

OffscreenCanvasと組み合わせることでCLI(Command Line Interface)ツールなどをブラウザ上から利用することができます。

↓その方針でやっているかは分からないですが、WebAssemblyを使ってブラウザでわいわいしている例

www.publickey1.jp

プロセスとスレッド

milestone-of-se.nesuke.com

Hatenaのアクセシビリティのイベントを見た

久しぶりにこういうのを見たメモ

Hatena Engineer Seminar #25 アクセシビリティ編 #hatenatech

www.youtube.com

hatena.connpass.com

開発リソースは有限である

これはほんと

他にも進めた方がいいように思える機能の開発はあるけど、この取り組みはどのくらいやるべきなのという疑問は出がち。

どこまでやればOKのラインなのか

  • 理想は100点はそう(100点て何)
  • 仮に合格ラインの60点とするなら60点はこのラインという対応内容の方針が立てよう
  • いい感じのラインが定義されていれば、そこを達成するために必要な知識の収集や見積もりがしやすくなる

成果を積極的にアピールする

  • 改善PRを積み上げていくとやった感でるよね
  • 社内のScrapBoxだったりNotionだったりに対応まとめがあると次にやる人がアクセスしやすい
  • 機能単位、ページ単位で改善するとユーザーに対してリリースできる形になるのか。すごーい

改善の一歩

  • キーボード操作に対応するのがおすすめ
  • 改善内容の結果が分かりやすく検証しやすい
  • 他のアクセシビリティ観点にも多くのいい影響がある

CI

  • Lintやその他のツールで見れる状態にしていけばやっていきやすそう

Pa11y CI でアクセシビリティテストを GitHub Actions で実行する

https://github.com/jsx-eslint/eslint-plugin-jsx-a11y

taba11y

それはそれとして画面触りながらうおーーー対応してる使いやすいーーー!!とかやるの楽しそう

公開されているアクセシビリティガイドライン

URL
デジタル庁 https://www.digital.go.jp/resources/introduction-to-web-accessibility-guidebook/
freee https://a11y-guidelines.freee.co.jp/
CyberAgent https://a11y-guidelines.ameba.design/

azukiazusa.dev

Neovimの設定を変えた 2023

だいたい年に一回くらいの頻度でVimの設定を大きく変更している。

前回変えたときは coc.nvim + fzf-preview.vim を軸にしたものから ddc.nvim + ddu.nvim + denops.vim を中心にしていた。

大きく変更するタイミング以外にも、どこからか見つけてくるプラグインや設定をマイナーチェンジとして追加している。たまに何かの拍子に壊れるタイミングが来て色々見なおす必要が出てきて、今回もそうなってしまった。

github.com

方針

  • 起動が早いと嬉しい
  • 見た目がいいと気分もいい
  • Vim互換はあまり考えない

軸にするプラグイン

補完

スニペット

ファジーファインダー

  • toggleterm.nvim All in oneな感じのファジーファインダー。extensionはまだ入れてない。部分的にddu.nvimを使うみたいにするかもしれない

ターミナル

カラースキーマ

世界に色を付けたいのでTreesitterに対応しているもので選ぶ。対応しているものと対応していないもので、どのくらい変わるか試してないので別のにするかもしれない。以前はneodark.vimを使っていた。

見た目系

便利系

結局denopsに依存している。

自分でプラグイン作るとしてもdenopsで書きたいしいいか、と思ってる。

終わり

ファイルの読み込み時間とキーマップを一覧するために1ファイルにまとめたら400行くらいになった。

起動時間は変更前後でどちらも300msくらいでそこまで変わらず。若干早くなった。改善の余地はありまくりそう。

マスタリングDeno Fresh メモ

⚠️これはドキュメントを読んだメモです。

テンション上げていけという気持ちでマスタリングという名をつけています。

fresh.deno.dev

Deno Freshとは

速度・信頼性・シンプルを掲げて作られた、Deno実装の次世代Webフレームワーク。ビルドステップがなくエッジレンダリングベース。TypeScriptやフロントエンド開発に必要な基本的な機能に設定を必要としないアイランドアーキテクチャを採用していて、デフォルトではクライアントはJavaScriptを必要としない

引用: https://fresh.deno.dev/
Fresh embraces the tried and true design of server side rendering and progressive enhancement on the client side.

よくわかっていない単語集

プロジェクト作成

❯ deno run -A -r https://fresh.deno.dev deno-fresh-project

  🍋 Fresh: the next-gen web framework.

Let's set up your new Fresh project.

Fresh has built in support for styling using Tailwind CSS. Do you want to use this? [y/N] y
Do you use VS Code? [y/N] y
The manifest has been generated for 3 routes and 1 islands.

Project initialized!

Enter your project directory using cd deno-fresh-project.
Run deno task start to start the project. CTRL-C to stop.

Stuck? Join our Discord https://discord.gg/deno

Happy hacking! 🦕

サーバー起動

❯ deno task start
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 3 routes and 1 islands.
Listening on http://localhost:8000/

Hot Module Reloadに対応していて、ファイルの変更保存を検知してブラウザページがリロードされる。

ディレクトリ構造

❯ tree .
.
├── README.md
├── components
│   └── Button.tsx
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│   └── Counter.tsx
├── main.ts
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
├── static
│   ├── favicon.ico
│   └── logo.svg
└── twind.config.ts

5 directories, 14 files

ファイル

deno.json : package.jsonのようなもの

dev.ts : プロジェクトのエントリーポイント。deno.jsonのタスクで指定される

fresh.gen.ts : Freshが生成するマニフェストファイル。アイランドアーキテクチャで読み込みするために使われてそう

import_map.json : Denoでよくライブラリ import/export をするファイル

ディレクト

routes : ファイルシステムベースのルーティング。アイランドアーキテクチャな分そのまま配信しているわけではなさそう

islands : インタラクティブアイランドを置くディレクト

components : コンポーネントを置く場所ではあるけどislandsとの違いは何だろう?

static : 静的ファイルを置くディレクト

ルート作成

routes ディレクトリ内に tsx ファイルを追加するとルーティングが追加される。

ドキュメントの例だと AboutPage という名前で関数を定義しているが default export で出していればなんでもいい。

引用: https://fresh.deno.dev/docs/getting-started/create-a-route

// routes/about.tsx

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>This is the about page.</p>
    </main>
  );
}

ダイナミックルーティング

[propName].tsx でファイル名を作成してページコンポーネントの引数でpropNameを受け取ることができる。

引用: https://fresh.deno.dev/docs/getting-started/dynamic-routes

// routes/[name].tsx

import { PageProps } from "$fresh/server.ts";

export default function GreetPage(props: PageProps) {
  const { name } = props.params;
  return (
    <main>
      <p>Greetings to you, {name}!</p>
    </main>);
}

ファイルシステムルーティングって

仕様もしくはそういうフレームワークあるのかな。

型で保護を受けたいみたいな視点でいうと難しさもあるらしい。

zenn.dev

カスタムハンドラー

ルートファイルの中で handler オブジェクトを名前付きエクスポートして適用する。 handler オブジェクトはHTTPリクエストメソッド毎に定義ができる。

⚠️ Next.jsと若干違うところ

Next.jsでは getStatisPropsgetServerSideProps といった関数を定義することで、リクエストに対してハンドラーのようなものを定義できる。

Next.jsでは getServerSideProps 後にページコンポーネントの処理に移る。対してFreshではハンドラー内で受け取る ctx を使って、ページコンポーネントのレンダー結果をレスポンスに含める。

引用: https://fresh.deno.dev/docs/getting-started/custom-handlers

// routes/about.tsx

import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    const resp = await ctx.render();
    resp.headers.set("X-Custom-Header", "Hello");
    return resp;
  },
};

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>This is the about page.</p>
    </main>);
}

ページコンポーネントではなく、JSONやファイルなどのデータを返したい場合は routes/api ディレクトリ以下でハンドラーのみ定義するのがいい。

データフェッチ

ハンドラー内で取得してきて ctx.render を呼び出すときに渡してやるといい。

フォーム送信

ReactとかVue使ってるとフォームもAPIで送信しがちだからハンドラーの方に GET or POST で返すの逆に珍しい。

クライアント側にJavaScript送らなくていいって言っているのこの辺りだよな。

サーバーサイドでレンダリングするとサーバーの負荷が高まる。ブラウザでレンダリングするとクライアントにJavaScriptがたくさん必要になる。サーバーサイドレンダリングからハイドレーションみたいなやり方でも、クライアント側でJavaScriptが多く必要な上に、サーバーとクライアントで同じような処理をするところが無駄無駄。

Freshのこのやり方はエッジサーバーで負荷分散の能力が高まってる前提があって、Denoがやり始めたCDN EdgeのDeno Deployとのシナジーがすごい。

deno.com

zenn.dev

フォーム送信についてブラウザがいいシステムを持ってる話は、ほんとにいいなと思う。扱っているシステムが素で持っている(追加されている)機能が結構強いものあったりする。TypeScriptに対するJavaScript本体とか。

引用: https://fresh.deno.dev/docs/getting-started/form-submissions

// routes/search.tsx

import { Handlers, PageProps } from "$fresh/server.ts";

const NAMES = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Frank"];

interface Data {
  results: string[];
  query: string;
}

export const handler: Handlers<Data> = {
  GET(req, ctx) {
    const url = new URL(req.url);
    const query = url.searchParams.get("q") || "";
    const results = NAMES.filter((name) => name.includes(query));
    return ctx.render({ results, query });
  },
};

export default function Page({ data }: PageProps<Data>) {
  const { results, query } = data;
  return (
    <div>
      <form>
        <input type="text" name="q" value={query} />
        <button type="submit">Search</button>
      </form>
      <ul>
        {results.map((name) => <li key={name}>{name}</li>)}
      </ul>
    </div>);
}

アイランドアーキテクチャの話

Deno Freshを使うと静的なページやユーザーアクションのないページに関してはJavaScriptのバンドルサイズ0が実現できる。けど現実にはそんなページはほとんどなくて、何かの機能を提供するボタンやリッチアニメーションなどの装飾のために少しだけJavaScriptが必要になる。

こういう静的なコンテンツの多いページの中に、ぽつぽつと島のようにJavaScriptが必要な要素があるみたいな形をアイランドアーキテクチャと呼んでるらしい。

Deno Freshでの islands ディレクトリで定義するコンポーネントはクライアントでレンダリング(というかハイドレーション)される。これパーシャルハイドレーション。

マークアップのみのコンポーネントであれば components ディレクトリに定義して、状態を持ったりするコンポーネントislands ディレクトリに定義することになりそう?

マークアップのみのコンポーネントであっても islands 内のコンポーネントから使われる場合もある。それを考慮するために $fresh/runtime.tsIS_BROWSER っていうフラグを持ってる。

import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";

export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
  return (
    <button
      {...props}
      disabled={!IS_BROWSER || props.disabled}
      class="px-2 py-1 border(gray-100 2) hover:bg-gray-200"
    />
  );
}

他にない感じの処理だ。

デプロイ先

GitHubにプッシュしてDeno Deploy使うといいよって。Cloudflare Workersとかもよく聞く。