先日の Supabase Edge Functionsに続いて、Netlify Edge Function が公開されました。
Supabase Edge Functions は、認証情報を要求することもありブラウザアクセスしてページを返すのは少し難しいようでした。
比較として Netlify Edge Function は、ブラウザアクセスもできるより Deno Deploy っぽいサービスとして使えるものになっていました。
というわけで、動作の確認もできたのでメモしておきます。
参考
準備
Deno - Netlify Edge Functions on Deno Deploy にある netlify-cli を使いたかったのですが、挫折。
(インストール時、どうしてもエラーになったのであきらめた。)
ということで、cli を使わずに進めます。
前提として以下の準備をします。
- github にリポジトリを作成(gitlab でもイイみたいだけど使ってないので…)
- Netlify にアカウントを作成
- Netlify でサイトを作成
- サイトの参照先リポジトリを 1. で作ったリポジトリにする
これで、github に push すると、Netlify でビルドが開始されてデプロイできる。
Netlify Edge Functions をデプロイ
git 管理したディレクトリの最小構成は次のようになります。
1 2 3 4 5 6
| $ tree . |-- netlify | `-- edge-functions | `-- first-function.ts `-- netlify.toml
|
netlify.toml の中身は次の通り。
netlify.toml1 2 3
| [[edge_functions]] function = "first-function" path = "/"
|
netlify/edge-functions/first-function.ts の中身は次の通り。
netlify/edge-functions/first-function.ts1
| export default () => new Response("First Function");
|
Deno Deploy との比較としてポイントになるのが、この関数のエクスポートだけで動作できる点。
ここまで用意できたら github に push します。
Netlify のページにアクセスするとデプロイ結果が出ています。
作成したサイトにアクセスすると、実装の通り次のように表示されています。
複数の関数のデプロイができその場合は、次のようになります。
netlify.toml(複数の関数をデプロイ)1 2 3 4 5 6 7
| [[edge_functions]] function = "first-function" path = "/"
[[edge_functions]] function = "second-function" path = "/second"
|
netlify/edge-functions/second-function.ts1
| export default () => new Response("Second Function");
|
記述した通り、/second にアクセスすると、次のようになります。
Netlify Edge Functions の特徴
Netlify Edge Functions の特徴として、独自拡張された Context があります。
Netlify-specific Context object
独自拡張されている内容として、次のようなものがあります。
- Cookies
- geo
- json
- log
- next()
- rewrite(url)
これらそれぞれの使い方については、Netlify - Edge Functions Examples - Edge Functions on Netlify に説明がありました。
ライブラリを使わなくても、Cookies が扱えるのは便利そうです。
(このあたりは、関数を直接エクスポートするという形式のため、既存の Context との互換性が取れなかったのでは?と感じる)
試しに、Cookies と geo を使ってみると、次のようになります。
netlify/edge-functions/third-function.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Context } from "netlify:edge";
export default async (request: Request, context: Context) => { let value = context.cookies.get("key");
if (!value) { value = "0"; }
context.cookies.set({ name: "key", value: `${Number(value) + 1}`, });
return context.json({ count: value, geo: context.geo, }); };
|
アクセスすると、次のレスポンスがあります。
netlify/edge-functions/third-function.ts のレスポンス1 2 3 4 5 6 7 8
| { "count": "14", // <= この部分はアクセス都度カウントアップ "geo": { "city": "Tokyo", "country": { "code": "JP", "name": "Japan" }, "subdivision": { "code": "13", "name": "Tokyo" } } }
|
ということで、アクセス元の情報の取得と、Cookies を使えました。
next() がよくわからない
next() の説明を読んでみると、チェーン内に次の関数が有る場合… などと書いてあります。
どういうこと?と感じますが、netlify.toml の書き方のドキュメントにヒントがあります。
見てみると、同じパスが複数の関数に割り当てされています。
実行は上から順にとあるので、動作を確認するには次のように用意します。
netlify.toml(next()の確認部分抜粋)1 2 3 4 5 6 7
| [[edge_functions]] function = "next1-function" path = "/next"
[[edge_functions]] function = "next2-function" path = "/next"
|
netlify/edge-functions/next1-function.ts1 2 3 4 5 6
| import { Context } from "netlify:edge";
export default async (req: Request, context: Context) => { const res = await context.next({ sendConditionalRequest: true }); return context.json({ exec: "next1-function", nextExec: await res.json() }); };
|
netlify/edge-functions/next1-function.ts1 2 3 4 5
| import { Context } from "netlify:edge";
export default async (req: Request, context: Context) => { return context.json({ exec: "next2-function" }); };
|
デプロイして、/next にアクセスすると、次のようになります。
1
| { "exec": "next1-function", "nextExec": { "exec": "next2-function" } }
|
ここまで動かしてみると、意味が分かります。
通常の deno deploy 相当のこともできるのか?
関数を直接エクスポートするのは、Netlify Edge Functions の独自拡張です。
既存 deno deploy 相当の普通のサーバーアプリのデプロイもできましたが。
次のように複数のサーバーを立てることはできませんでした。
netlify.toml(サーバーを2つデプロイ)1 2 3 4 5 6 7
| [[edge_functions]] function = "server1-function" path = "/server1"
[[edge_functions]] function = "server2-function" path = "/server2"
|
netlify/edge-functions/server1-function.ts1 2 3 4 5 6 7
| import { serve } from "https://deno.land/std@0.136.0/http/server.ts";
serve((_req) => { return new Response("Server 1", { headers: { "content-type": "text/plain" }, }); });
|
netlify/edge-functions/server2-function.ts1 2 3 4 5 6 7
| import { serve } from "https://deno.land/std@0.136.0/http/server.ts";
serve((_req) => { return new Response("Server 2", { headers: { "content-type": "text/plain" }, }); });
|
それぞれのサーバーでポートを変えてもダメ。
さらに netlify.toml で定義をしていなくても、デプロイされるファイル群に serve が 2 箇所有るだけで動かなくなりました。
上の例では、netlify/edge-functions/server2-function.ts が存在しているだけでエラーになります。
Netlify のビルドログを見ると、次のように記載されています。
1 2 3 4 5 6 7 8 9 10 11 12
| 4:39:09 PM: ──────────────────────────────────────────────────────────────── 4:39:09 PM: 1. Edge Functions bundling 4:39:09 PM: ──────────────────────────────────────────────────────────────── 4:39:09 PM: 4:39:09 PM: Packaging Edge Functions from netlify/edge-functions directory: 4:39:09 PM: - first-function 4:39:09 PM: - next1-function 4:39:09 PM: - next2-function 4:39:09 PM: - second-function 4:39:09 PM: - server1-function 4:39:09 PM: - server2-function 4:39:09 PM: - third-function
|
おそらく、すべてのファイルを読み込むときに、すべて実行されているのでは?と考えられます。
サーバーアプリケーションを立てるときは1 つだけ書くようにする必要があります。
複数サーバー立てられたらいいなと思いましたが、そうは上手くいかないものです。
さらに、関数エクスポートする Netlify Edge Functions 形式と server アプリケーションとの共存もできないものでした。
パスの設定にもよるのでしょうが、処理がすべてサーバーアプリケーションに吸われてしまう様でした。
レスポンスが、サーバーアプリケーションからのレスポンスしか確認できませんでした。
tsx も使えるの?
ビルドログを確認すると、.tsx ファイルは読み込み対象にならないようだったので、関数自体は.ts ファイルを使用。
tsx はモジュールとして呼び出して使用しました。
レンダリングは ReactDOMServer を使いました。
以下のファイルの用意をします。
netlify/edge-functions/views/page.tsx1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React from "https://esm.sh/react"; import ReactDOMServer from "https://esm.sh/react-dom/server";
function Template(props: { children: React.ReactNode }) { return ( <html> <head> <title>Page from Netlify Edge Functions</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossOrigin="anonymous" ></link> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossOrigin="anonymous" ></script> </head> <body> <div className="container-md">{props.children}</div> </body> </html> ); }
export default function Page() { return ReactDOMServer.renderToString( <Template> <h1>Tsx-Function-Page</h1> </Template> ); }
|
netlify/edge-functions/tsx-function.tsx1 2 3 4
| import Page from "./views/page.tsx";
export default () => new Response(Page(), { headers: { "content-type": "text/html" } });
|
1 2 3
| [[edge_functions]] function = "tsx-function" path = "/tsx/"
|
デプロイ後、/tsx/ にアクセスすると次のようになります。
パスルーティングできる?
できる。
netlify.toml(全リクエストをroute-functionへ)1 2 3
| [[edge_functions]] function = "route-function" path = "/*"
|
netlify/edge-functions/route-function.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Context } from "netlify:edge";
export default async (req: Request, context: Context) => { if (new URLPattern({ pathname: "/a/" }).test(req.url)) { return context.json({ url: req.url, route: "A" }); } if (new URLPattern({ pathname: "/b/" }).test(req.url)) { return context.json({ url: req.url, route: "B" }); } if (new URLPattern({ pathname: "/c/:id([1-9]+)" }).test(req.url)) { const match = new URLPattern({ pathname: "/c/:id([1-9]*)" }).exec(req.url); if (!match) return new Response("ERROR");
const { groups } = match.pathname; const id = groups["id"]; const idNumber = parseInt(id, 10);
return context.json({ url: req.url, route: "C", id: idNumber }); } };
|
これをデプロイしてアクセスすると次のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| $ curl https://xxxxxxxxxxxxxx.netlify.app/a/ -s |jq . -c {"url":"https://xxxxxxxxxxxxxx.netlify.app/a/","route":"A"}
$ curl https://xxxxxxxxxxxxxx.netlify.app/b/ -s |jq . -c {"url":"https://xxxxxxxxxxxxxx.netlify.app/b/","route":"B"}
$ curl https://xxxxxxxxxxxxxx.netlify.app/c/ -s |jq . -c parse error: Invalid numeric literal at line 1, column 10
$ curl https://xxxxxxxxxxxxxx.netlify.app/c/123 -s |jq . -c {"url":"https://xxxxxxxxxxxxxx.netlify.app/c/123","route":"C","id":123}
$ curl https://xxxxxxxxxxxxxx.netlify.app/c/123a -s |jq . -c parse error: Invalid numeric literal at line 1, column 10
|
ちゃんとパスルーティングもできる。
ただし、パスのリライト?する機能は無いようなので、netlify.toml で path = “/a/*“ と書くと /a/しか応答できなくなる。
引っかかったポイント
Netlify Edge Functions は、netlify.toml に定義が無くても netlify/edge-functions に置いてある .ts(.js) を読み込んでいる様です。
それ故か、default export がされていないファイルが有ると全体でエラーを起こすようです。
(ここにハマって 1 時間かかった)
また、1 回 .tsx でデプロイすると、.ts に直しても差分検知しないようで、ファイル名を直さないとデプロイできなくなりました。
Netlify Edge Functions を触ってみました。
触った所感としては、deno deploy と同じようにサーバーアプリはデプロイできるけど、止めた方がいい。
Netlify Edge Functions の拡張した利点が全部死ぬから。という感じ。
この拡張用にserverless_oakみたいな oak の関連モジュールもそのうちだれか作りそう。
ドキュメントを見ると、Next や Nuxt、SvelteKit などが動く?らしく、引き続き調べたいところです。
ではでは。