Hono[炎] Ultrafast web framework for Cloudflare Workers.

Yusuke Wada

2022-03-05 YAPC::Japan::Online 2022

今日話すこと

  1. Cloudflare Workers
  2. Honoの特徴
  3. HonoのAPI
  4. Honoを使う
  5. Service Worker Magic

はじめに

Initial commitは2.5ヶ月前

2021-12-15

SS

現在のVersion

v0.5.1

Contributors

Con

SS

SS

作るの楽しい!!

Cloudflare Workersについて

CloudflareのCDNエッジで実行されるサーバーレス環境

速い・短い・小さい

  • コールドスタートなし
  • CPU時間10ms(Free、Bundled)
  • 1MB以内

CPU時間

スクリプトが実行中でCPUリソースを使用している時間。

SS

その他のキーワード

  • Workers KV
  • Workers Durable Objects
  • Workers Sites
  • Cache API
  • CDNのキャッシュコントロール

ユースケース

1.PC/SPの振り分け

let isMobile = false
const userAgent = request.headers.get("User-Agent") || ""
if (userAgent.match(/(iPhone|Android|Mobile)/)) {
  isMobile = true
}

const device = isMobile ? "Mobile" : "Desktop"

const cacheUrl = new URL(request.url + "-" + device)
const cacheKey = new Request(reqeust.url + device, request)
const cache = caches.default

let response = await cache.match(cacheKey)

2.Slackスラッシュコマンド

const karma = async (name: string, operation: string) => {
  const key = PREFIX + name
  const karm = await KV_KARMA.get(key)

  if (operation == "++") {
    karma = karma + 1
  } else {
    karma = karma - 1
  }
  await KV_KARMA.put(key, karma)

  return `${name} : ${karma}`
}

3.ブックマークアプリ

https://github.com/yusukebe/marks

SS

Worker

特徴というか制限

  • JavaScript (Rustもサポート)
  • Node.jsではない
  • APIが限られている

Service Workerで書く

Service Workerとは?

Service Worker はブラウザが Web ページとは別にバックグラウンドで実行するスクリプトで、Web ページやユーザーのインタラクションを必要としない機能を Web にもたらします。 https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja

  • プッシュ通知
  • オフラインでも閲覧できる
  • バックグラウンドのデータ同期
  • キャッシュ

fetchイベントを書く

const handleRequest = async (request) => {
  return new Response("Hello YAPC!")
}
addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request))
})

つかえるAPI

  • Encoding
  • Fetch / FetchEvent
  • HTMLRewriter
  • Headers
  • Request / Response
  • Streams
  • Web Crypto
  • Web standards
  • WebSockets
  • Cache / KV / Durable Objects

Fastly Compute@Edgeでも同じコードが動くことがある

Wrangler

  • 開発〜デプロイ
  • wrangler dev
  • wrangler publish

ちなみにPerlで書ける

Perlito

JS::inline('addEventListener("fetch", event => { p5cget("main", "listener")([event]) })');
my $url = URL->new($req->url);
my $query_string = $url->search;
...
my $headers = Headers->new();
...
return Response->new(
  $msg,
    {
      status  => 200,
      headers => $headers
    }
);

Cloudflare Workers🔥でもPerl🐫でも動くPerlを書く

https://yusukebe.com/posts/2021/psgi-cloudflare-workers/

Honoについて

1分で分かるHono

Hono

4ステップで開発〜デプロイ

$ yarn init -y
$ wrangler init
$ touch index.js // Write code
$ wrangler publish

{project-name}.{user-name}.workers.dev

Honoの特徴

モチベーション

「Webサイトを作ろうとしていたら、フレームワークを作っていた」

他にも…Cloudflare向けのルーター・フレームワーク

  • itty-router - 37行
  • Sunder - 先発
  • worktop - Cloudflareの中の人が作ってる

アイデンティティを探す

「Features」

  • Ultrafast
  • Zero dependencies
  • Middleware
  • Optimized
  • (TypeScript)

Ultrafast

ベンチマーク

TrieRouter

hono x 779,197 ops/sec ±6.55% (78 runs sampled) <---
itty-router x 161,813 ops/sec ±3.87% (87 runs sampled)
sunder x 334,096 ops/sec ±1.33% (93 runs sampled)
worktop x 212,661 ops/sec ±4.40% (81 runs sampled)
Fastest is hono

Utrafast!

RegExpRouter

hono x 723,504 ops/sec ±6.76% (63 runs sampled)
hono with RegExpRouter x 934,401 ops/sec ±5.49% (68 runs sampled) <---
itty-router x 160,676 ops/sec ±3.23% (88 runs sampled)
sunder x 312,128 ops/sec ±4.55% (85 runs sampled)
worktop x 209,345 ops/sec ±4.52% (78 runs sampled)
Fastest is hono with RegExpRouter

Utrafast!!!

他のnode.jsルーターと比べても…

  • 有名どころに圧勝
express benchmark (includes handling) x all together: 292,090 ops/sec
koa-router benchmark x all together: 232,845 ops/sec
hono RegExpRouter benchmark x all together: 1,426,009 ops/sec <---
  • find-my-wayに勝った => Frameworkよりだから遅い
  • trek-routerに負けた => RegexとMulti paramに対応しない
  • hono RegExpRouter => RegexとMulti paramに対応
find-my-way benchmark x all together: 1,059,323 ops/sec
trek-router benchmark x all together: 1,439,378 ops/sec
hono RegExpRouter benchmark x all together: 1,426,009 ops/sec <---

ほぼ最強

なぜそんなに速いのか?

TrieRouter

Inspired by goblin.

トライ木

class Node<T> {
  method: string
  handler: T
  children: Record<string, Node<T>>
}

RegExpRouter

Inspired by Router::Boom.

Introduce RegExpRouter #109 by @usualoma

全てのルートをひとつの大きな正規表現にする

  • /help
  • /:user_id/followees
  • /:user_id/followers
  • /:user_id/posts
  • /:user_id/posts/:post_id
  • /:user_id/posts/:post_id/likes
^/(?:help$()|([^/]+)/(?:followe(?:es$()|rs$())|posts(?:/([^/]+)(?:/likes$()|$())|$())))

だからHonoは速い

その他の特徴

Request/Responseの扱い

Perlの場合

例えばPlackを使う

use Plack::Request;
 
my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
 
    my $path_info = $req->path_info;
    my $query     = $req->parameters->{query};
 
    my $res = $req->new_response(200);
    $res->finalize;
};

Request/Responseオブジェクトつくりがち

Cloudflare Workersの場合

そもそもRequest/Responseオブジェクトが提供されている


const handleRequest = (req: Request) => {
  const ua = req.headers.get('User-Agent')
  new res = new Response(`You are ${ua}`, {
    headers: {
      'X-Message': 'Hello YAPC!',
    }
  })
  return res
}

Contextがショートカットを提供する

  • c.req.header(name)
  • c.header(name, value)
  • c.json(object)
app.get('/hello', (c) => {
  const ua = c.req.header('User-Agent')
  c.header('X-Message', 'Hello YAPC!')
  return c.json({ 'You are ': ua })
})

Middleware

Inspired by koa

Everything is middleware - “Koa”

Middlewareを書く

app.use('*', async (c, next) => {
  const start = Date.now()
  // ^--- handle request
  await next() // <--- dispatch handler
  // v--- handle response
  const ms = Date.now() - start
  c.header('X-Response-Time', `${ms}ms`)
})

// a handler
app.get('/hello', (c) => c.text('Hello YAPC!'))

HandlerをMiddlewareが包む

app.use('*', async (c, next) => {
  console.log('Foo - before')
  await next()
  console.log('Foo - after')
})

app.use('*', async (c, next) => {
  console.log('Bar - before')
  await next()
  console.log('Bar - after')
})
app.get('/hello', (c) => {
  console.log('Handler')
  return c.text('Hello YAPC!')
})

SS

HonoのAPI

app

  • app.HTTP_METHOD(path, handler)
  • app.all(path, handler)
  • app.route(path)
  • app.use(path, middleware)
  • app.notFound(handler)
  • app.onError(err, handler)
  • app.fire()
  • app.fetch(request, env, event)

Routing

app.HTTP_METHOD / app.all

// HTTP Methods
app.get('/', (c) => c.text('GET /'))
app.post('/', (c) => c.text('POST /'))

// Wildcard
app.get('/wild/*/card', (c) => {
  return c.text('GET /wild/*/card')
})

// Any HTTP methods
app.all('/hello', (c) => c.text('Any Method /hello'))

Named Parameter

app.get('/user/:name', (c) => {
  const name = c.req.param('name')
  ...
})

Named

Added type to c.req.param key. #102 by @usualoma

Regexp

app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
  const date = c.req.param('date')
  const title = c.req.param('title')
  ...

Nested route

const book = app.route('/book')
book.get('/', (c) => c.text('List Books')) // => GET /book
book.get('/:id', (c) => {
  // => GET /book/:id
  const id = c.req.param('id')
  return c.text('Get Book: ' + id)
})
book.post('/', (c) => c.text('Create Book')) // => POST /book

no strict

If strict is set false, /helloand/hello/ are treated the same:

const app = new Hono({ strict: false })

app.get('/hello', (c) => c.text('/hello or /hello/'))

async/await

app.get('/fetch-url', async (c) => {
  const response = await fetch('https://example.com/')
  return c.text(`Status is ${response.status}`)
})

Middleware

Builtin Middleware

import { Hono } from 'hono'
import { poweredBy } from 'hono/powered-by'
import { logger } from 'hono/logger'
import { basicAuth } from 'hono/basicAuth'

const app = new Hono()

app.use('*', poweredBy())
app.use('*', logger())
app.use('/auth/*', basicAuth({ username: 'hono', password: 'acoolproject' }))

Available builtin middleware

  • basic-auth <— 1から作るとダルい
  • body-parse
  • cookie
  • cors
  • etag
  • logger
  • mustache <— テンプレート
  • powered-by
  • serve-static <— KVを使う

Custom Middleware

// Custom logger
app.use('*', async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`)
  await next()
})

// Add a custom header
app.use('/message/*', async (c, next) => {
  await next()
  await c.header('x-message', 'This is middleware!')
})

app.get('/message/hello', (c) => c.text('Hello Middleware!'))

Not Found

app.notFound((c) => {
  return c.text('Custom 404 Message', 404)
})

Error Handling

app.onError((err, c) => {
  console.error(`${err}`)
  return c.text('Custom Error Message', 500)
})

Context

c.req

app.get('/shortcut', (c) => {
  const userAgent = c.req.header('User-Agent')
  ...
})

app.get('/search', (c) => {
  const query = c.req.query('q')
  ...
})

app.get('/entry/:id', (c) => {
  const id = c.req.param('id')
  ...
})

Shortcuts for Response

app.get('/welcome', (c) => {
  c.header('X-Message', 'Hello!')
  c.header('Content-Type', 'text/plain')
  c.status(201)

  return c.body('Thank you for comming')
})

以下と同じ

return new Response('Thank you for comming', {
  status: 201,
  statusText: 'Created',
  headers: {
   'X-Message': 'Hello',
    'Content-Type': 'text/plain',
    'Content-Length': '22'
  }
})

c.text()

Render text as Content-Type:text/plain:

app.get('/say', (c) => {
  return c.text('Hello!')
})

c.json()

Render JSON as Content-Type:application/json:

app.get('/api', (c) => {
  return c.json({ message: 'Hello!' })
})

c.html()

Render HTML as Content-Type:text/html:

app.get('/', (c) => {
  return c.html('<h1>Hello! Hono!</h1>')
})

c.redirect()

Redirect, default status code is 302:

app.get('/redirect', (c) => c.redirect('/'))
app.get('/redirect-permanently', (c) => c.redirect('/', 301))

c.res

// Response object
app.use('/', (c, next) => {
  next()
  c.res.headers.append('X-Debug', 'Debug message')
})

c.event

// FetchEvent object
app.use('*', async (c, next) => {
  c.event.waitUntil(
    ...
  )
  await next()
})

c.env

// Environment object for Cloudflare Workers
app.get('*', async c => {
  const counter = c.env.COUNTER
  ...
})

fire

app.fire() do:

addEventListener('fetch', (event) => {
  event.respondWith(this.handleEvent(event))
})

fetch

app.fetch() is for Cloudflare Module Worker syntax.

export default {
  fetch(request: Request, env: Env, event: FetchEvent) {
    return app.fetch(request, env, event)
  },
}

/*
or just do this:
export default app
*/

use Hono

Hono Starter

wrangler generate my-app https://github.com/yusukebe/hono-minimal

SS

.
├── README.md
├── package.json
├── src
│   └── index.ts
└── wrangler.toml
  "dependencies": {
    "hono": "^0.5.1"
  },
  "devDependencies": {
    "esbuild": "^0.14.23",
    "miniflare": "2.2.0"
  }

Hono Examples

  • basic
  • blog
  • compute-at-edge
  • durable-objects
  • jsx-ssr
  • serve-static

Not only for Web API

「家系ラーメン食べたい!」

ie

ie

家系ラーメン食べたい!

mustacheブランチ

  • Hono
  • serve-static Middleware
  • mustache Middleware
  • 開発・デプロイにWrangler 2.0
import { Hono } from 'hono'
import { mustache } from 'hono/mustache'
import { serveStatic } from 'hono/serve-static'
import { ies } from './ies'
app.use('*', mustache({ root: 'view' }))
app.use('/static/*', serveStatic({ root: 'public' }))

app.use('/static/*', async (c, next) => {
  await next()
  if (c.res.headers.get('Content-Type').match(/image/)) {
    c.header('Cache-Control', 'public, max-age=86400')
  }
})
const partials = { header: 'header', footer: 'footer' }

app.get('/', (c) => {
  return c.render('index', { ies: ies }, partials)
})

app.get('/ie/:name', (c) => {
  const name = c.req.param('name')
  const ie = ies.find((i) => i.name === name)
  if (!ie) return c.notFound()
  return c.render('ie', ie, partials)
})
app.fire()

家系ラーメン食べたい!

reactブランチ

  • Honoを使っている
  • ReactSSRしている(クライアントは何もしてない)
  • microCMSでコンテンツを管理
  • APIレスポンスはKVでキャッシュ
  • Webhookを受け取って、キャッシュをパージ
  • 開発にmifnilare 2.x、デプロイにはWrangler 2.0

CDNエッジでも「ちゃんとした」Webがつくれる

https://iekei.yusukebe.workers.dev/

Service Worker Magic

  • Server (Cloudflare Workers) code is sw.js.
  • Browser ( Service Worker ) code is sw.js.
  • Cloudflare Workers sw.js serves sw.js.
  • Service Worker sw.js is registered on /.
  • The scope is /sw/*.
  • /server/hello => from the server.
  • /sw/hello => from the browser.
  • Request is intercepted by Service Worker.

Magic

https://service-worker-magic.yusukebe.workers.dev

Enjoy Hono!

https://github.com/yusukebe/hono

おしまい