URLPattern を Deno で試してみる 🦕
こんにちは、最近自分の中で Deno が熱いです。
Deno は JavaScript / TypeScript のランタイムであり、Node.js の作者である Ryan Dahl 氏が 2018年に行った「Node.jsに関する10の反省点」という発表の中で発表されました。
強力エコシステムや Permission 設定によるセキュリティの強化、ブラウザとの互換性など魅力的です。
あとロゴの恐竜がかわいいです(大事)。
そんな訳で今回は Deno で遊んでみようと思います
はじめに
先日 9/14 に Deno の v1.14 がリリースされました🎉 (Release Notes)
日本語だとこちらの記事がわかりやすいです。
zenn.dev
自分が気になったトピックはこの辺です。
deno lint
、deno fmt
のスタイルを設定できるようになったURLPattern
が実装された- URL の parse が3倍速くなった
また、同日に Deno の標準ライブラリである deno_std の v0.107.0 もリリースされました 🎉
こちらは collections モジュールの機能追加と http モジュールの改良が主な変更点です。
Deno v1.13 で HTTP Server のネイティブ実装が stable になりました。これによって std/http は削除される予定*1だったのですが、今回ネイティブ実装を使うようになったため std/http は残されるようです。*2 また、ネイティブ実装を利用することでパフォーマンスが大幅に改善しています。
今回のリリースでは Deno で Web Server を実装するにあたって嬉しい内容が多いなという感想です。
この記事では新たに追加された URLPattern API を用いて、外部のライブラリに頼らずルーティングを行う例を示します。
簡単な Web Server であれば標準ライブラリだけで割と書けるようになっています。
URLPattern とは
まず URLPattern
とは何かについて紹介したいと思います。
URLPattern は新しめの WebAPI で、URL によるルーティングパターンを標準化するためのものです。
ルーティングで使われるパターンには明確な標準はありませんが正規表現やワイルドカード、名前付きのトークングループなどがよく使われます。
URLPattern で構文を定義することによって、ルーティングパターンの再発明を避けることが目的です。
いくつか例を挙げると次のようなパターンが利用できます。
Ruby on Rails や Express などのフレームワークを使ったことのある方にとっては馴染みのある表記ではないでしょうか。
引用: https://pr8734.content.dev.mdn.mozit.cloud/en-US/docs/Web/API/URLPattern
引用元のMDN(draft?)で URLPatter の Browser Compatibility を見てみると利用できるブラウザは現時点(2021/09/17)で Chrome、Edge だけようです。
ちなみに表中にはないですが、Node.js ではまだ利用できないようです。
使い方
ここでは簡単に URLPattern の使い方を説明します。
URLPattern はその名の通り(判定したい) URL のパターンをプロパティとして持ち、正規表現を扱う RegExp のように exec
や test
といった実際にパターンマッチを行うためのメソッドを持つクラスです。
const pattern1 = new URLPattern("https://example.com/users/*"); const pattern2 = new URLPattern("https://example.com/users/:id"); const pattern3 = new URLPattern({ pathname: "/users/:id" }); console.log(pattern1.test("https://example.com/users")); // false console.log(pattern1.test("https://example.com/users/123")); // true console.log(pattern2.exec("https://example.com/users")); // null console.log(pattern2.exec("https://example.com/users/123")?.pathname.groups); // { id: "123" } console.log(pattern3.exec("https://example.com/users/123")?.pathname.groups); // { id: "123" }
test
メソッドは、与えられた文字列がパターンにマッチするかを判定し boolean
を返します。
exec
メソッドは、与えられた文字列がパターンにマッチするかを判定しマッチした場合は URLPatternResult
というオブジェクトを返し、マッチしなかった場合は null
を返します。
ここで得られる URLPatternResult は pathname.groups
に名前付きグループにマッチした部分を抽出しているのですが Record<string, string>
型となっています。
他にもたくさんのパターンが利用できますが、ここでは省略いたします。
利用例
次に Deno でシンプルな Web Server を立て、そこで URLPattern を利用してみたいと思います。
Deno 公式のマニュアル を見てみると Deno 本体にある HTTP API を用いたサーバーの実装例と標準ライブラリ std/http を用いたサーバーの実装例を見つけることができます。
今回はせっかくなのでネイティブ実装の API を使うようになって速くなった std/http を使ってみようと思います。
一旦公式マニュアルのものをそのまま引用します。
webserver.ts
import { listenAndServe } from "https://deno.land/std@0.107.0/http/server.ts"; const addr = ":8080"; const handler = (request: Request): Response => { let body = "Your user-agent is:\n\n"; body += request.headers.get("user-agent") || "Unknown"; return new Response(body, { status: 200 }); }; console.log(`HTTP webserver running. Access it at: http://localhost:8080/`); await listenAndServe(addr, handler);
ルーティングなど無いシンプルなサーバーです。
deno run --allow-net webserver.ts
で起動できます。
適当な path にアクセスすると Request Header から User-Agent を取得し返します。
これをベースに URLPattern を使ってみようと思います。
webserver_with_routing.ts
import { listenAndServe } from "https://deno.land/std@0.107.0/http/server.ts"; const addr = ":8080"; const patternsToHandlers = new Map([ [{ pathname: "/" }, rootHandler], [{ pathname: "/ping" }, pingHandler], [{ pathname: "/users/:id" }, userHandler], ]); const routingMap = new Map(); for (const [pathPattern, handler] of patternsToHandlers) { const compiledPattern = new URLPattern(pathPattern); routingMap.set(compiledPattern, handler); } const routingHandler = (request: Request): Response => { for (const [pattern, handler] of routingMap) { const matched = pattern.exec(request.url); if (matched) { return handler(request, matched); } } return new Response(null, { status: 404 }); }; console.log(`HTTP webserver running. Access it at: http://localhost${addr}/`); await listenAndServe(addr, routingHandler); // request handlers function rootHandler(req: Request, matchRes: URLPatternResult | null) { return new Response("Hello, World!", { status: 200 }); } function pingHandler(req: Request, matchRes: URLPatternResult | null) { return new Response("pong", { status: 200 }); } function userHandler(req: Request, matchRes: URLPatternResult | null) { return new Response(`User ID: ${matchRes?.pathname.groups.id}`, { status: 200 }); }
いくつか新しい path を生やしてみました。
deno run --unstable --allow-net webserver_with_routing.ts
で起動できます。
これは次のような挙動をします。
/
にアクセスしたときは "Hello, World!" を返す/ping
にアクセスしたときは "pong" を返す/users/:id
にアクセスしたときはUserID: <id>
を返す(は path param) - 上記以外の場合は status code 404 を返す
handler のインターフェースなど改善したい気持ちはありますが、基本的なルーティングを行うことができました。
おまけ:簡易 Router の実装
上で実装した例では HTTP Method による分岐は実装しておらず、それぞれの handler 内部で実装する必要がありました。
ここでは簡易的なルーティングを行うクラスを実装し、HTTP Method による分岐も実装してみたいと思います。
以下のような Router クラスを実装します。
routers オブジェクトでは HTTP Method ごとの routing map を持つようにし get
、post
などのインスタンスメソッド経由でルーティングを定義します。
router.ts
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'; type Handler = (req: Request, urlRes: URLPatternResult | null) => Response; type RoutingMap = Map<URLPattern, Handler>; export class Router { routers: Record<HTTPMethod, RoutingMap>; constructor() { this.routers = { 'GET': new Map<URLPattern, Handler>(), 'POST': new Map<URLPattern, Handler>(), 'PUT': new Map<URLPattern, Handler>(), 'PATCH': new Map<URLPattern, Handler>(), 'DELETE': new Map<URLPattern, Handler>(), 'HEAD': new Map<URLPattern, Handler>(), }; } get(pathname: string, handler: Handler) { this.routers["GET"].set(new URLPattern({ pathname }), handler); return this; } post(pathname: string, handler: Handler) { this.routers["POST"].set(new URLPattern({ pathname }), handler); return this; } put(pathname: string, handler: Handler) { this.routers["PUT"].set(new URLPattern({ pathname }), handler); return this; } patch(pathname: string, handler: Handler) { this.routers["PATCH"].set(new URLPattern({ pathname }), handler); return this; } delete(pathname: string, handler: Handler) { this.routers["DELETE"].set(new URLPattern({ pathname }), handler); return this; } head(pathname: string, handler: Handler) { this.routers["HEAD"].set(new URLPattern({ pathname: pathname }), handler); return this; } route(req: Request): Response { try { for (const [pattern, handler] of this.routers[req.method as HTTPMethod]) { const matched = pattern.exec(req.url); if (matched) { return handler(req, matched); } } return new Response(null, { status: 404 }); } catch(e) { console.error(e); return new Response(null, { status: 500 }); } } }
これを使うと以下ようにルーティングを記述することができます。
server.ts
import { listenAndServe } from "https://deno.land/std@0.107.0/http/server.ts"; import { Router } from "./router.ts"; const addr = ":8080"; const router = new Router() .get("/users/:id", showUserHandler) .post("/users", createUserHandler) .put("/users/:id", updateUserHandler) .patch("/users/:id", updateUserHandler) .delete("/users/:id", deleteUserHandler); console.log(`HTTP webserver running. Access it at: http://localhost${addr}/`); await listenAndServe(addr, (req) => router.route(req)); // request handlers function showUserHandler(req: Request, urlRes: URLPatternResult | null) { return new Response(`Get the user: ${urlRes?.pathname.groups.id}`, { status: 200 }); } function createUserHandler(req: Request, urlRes: URLPatternResult | null) { return new Response(`Create a user`, { status: 201 }); } function updateUserHandler(req: Request, urlRes: URLPatternResult | null) { return new Response(`Update the user: ${urlRes?.pathname.groups.id}`, { status: 201 }); } function deleteUserHandler(req: Request, urlRes: URLPatternResult | null) { return new Response(`Delete the user: ${urlRes?.pathname.groups.id}`, { status: 201 }); }
~/URLPattern ❯❯❯ curl "localhost:8080/ping" pong% ~/URLPattern ❯❯❯ curl -X"GET" "localhost:8080/users/123" Get the user: 123% ~/URLPattern ❯❯❯ curl -X"POST" "localhost:8080/users" Create a user% ~/URLPattern ❯❯❯ curl -X"PUT" "localhost:8080/users/123" Update the user: 123% ~/URLPattern ❯❯❯ curl -X"PATCH" "localhost:8081/users/123" Update the user: 123% ~/URLPattern ❯❯❯ curl -X"DELETE" "localhost:8080/users/123" Delete the user: 123% ~/URLPattern ❯❯❯ curl -X"GET" -s -o /dev/null -w "%{http_code}" "localhost:8080/" 404%
まとめ
Deno v1.14 で追加された URLPattern という WebAPI の紹介を行い、Deno で簡単なルーティングを行う例を示しました。
URLPattern 自体によってルーティング処理がすごく書きやすくなった!という印象はないですが、ルーティングのパターンを統一することの意義は大きいと思います。
また、最後に実装した Router では HTTP Method と URL のパターンによるルーティングを行えるようにしました。 Router を自分で実装するならどうするか?という部分は面白かったですが、 エラーハンドリングやロギングなどを考えると普通に oak などのフレームワークを使いたくなりました 😇
フレームワークを使って開発をしていると URLPattern API を直接触ることはなかなか無いかもしれませんが、覚えているといざというとき役に立つかもしれないですね。
References
- URLPattern brings routing to the web platform
- draft: https://pr8734.content.dev.mdn.mozit.cloud/en-US/docs/Web/API/URLPattern
*1:https://deno.com/blog/v1.13#stabilize-native-http-server-api では"If you are currently using std/http we encourage you to upgrade to the native HTTP server. std/http will be available in std for a few more releases, but will be removed soon. " と書かれていて、この時点では削除する予定でした