Yusuke Wada
2022-03-05 YAPC::Japan::Online 2022
Initial commit
は2.5ヶ月前2021-12-15
v0.5.1
作るの楽しい!!
CloudflareのCDNエッジで実行されるサーバーレス環境
スクリプトが実行中でCPUリソースを使用している時間。
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)
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}`
}
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))
})
Fastly Compute@Edgeでも同じコードが動くことがある
wrangler dev
wrangler publish
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を書く
$ yarn init -y
$ wrangler init
$ touch index.js // Write code
$ wrangler publish
{project-name}.{user-name}.workers.dev
「Webサイトを作ろうとしていたら、フレームワークを作っていた」
他にも…Cloudflare向けのルーター・フレームワーク
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
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
他の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 <---
ほぼ最強
Inspired by goblin.
トライ木
class Node<T> {
method: string
handler: T
children: Record<string, Node<T>>
}
Inspired by Router::Boom.
Introduce RegExpRouter #109 by @usualoma
全てのルートをひとつの大きな正規表現にする
^/(?:help$()|([^/]+)/(?:followe(?:es$()|rs$())|posts(?:/([^/]+)(?:/likes$()|$())|$())))
だからHonoは速い
例えば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オブジェクトつくりがち
そもそも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
}
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 })
})
koa
Everything is middleware - “Koa”
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!')
})
app
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'))
app.get('/user/:name', (c) => {
const name = c.req.param('name')
...
})
Added type to c.req.param key. #102 by @usualoma
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
const date = c.req.param('date')
const title = c.req.param('title')
...
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
If strict
is set false
, /hello
and/hello/
are treated the same:
const app = new Hono({ strict: false })
app.get('/hello', (c) => c.text('/hello or /hello/'))
app.get('/fetch-url', async (c) => {
const response = await fetch('https://example.com/')
return c.text(`Status is ${response.status}`)
})
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' }))
// 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!'))
app.notFound((c) => {
return c.text('Custom 404 Message', 404)
})
app.onError((err, c) => {
console.error(`${err}`)
return c.text('Custom Error Message', 500)
})
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')
...
})
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'
}
})
Render text as Content-Type:text/plain
:
app.get('/say', (c) => {
return c.text('Hello!')
})
Render JSON as Content-Type:application/json
:
app.get('/api', (c) => {
return c.json({ message: 'Hello!' })
})
Render HTML as Content-Type:text/html
:
app.get('/', (c) => {
return c.html('<h1>Hello! Hono!</h1>')
})
Redirect, default status code is 302
:
app.get('/redirect', (c) => c.redirect('/'))
app.get('/redirect-permanently', (c) => c.redirect('/', 301))
// Response object
app.use('/', (c, next) => {
next()
c.res.headers.append('X-Debug', 'Debug message')
})
// FetchEvent object
app.use('*', async (c, next) => {
c.event.waitUntil(
...
)
await next()
})
// Environment object for Cloudflare Workers
app.get('*', async c => {
const counter = c.env.COUNTER
...
})
app.fire()
do:
addEventListener('fetch', (event) => {
event.respondWith(this.handleEvent(event))
})
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
wrangler generate my-app https://github.com/yusukebe/hono-minimal
.
├── README.md
├── package.json
├── src
│ └── index.ts
└── wrangler.toml
"dependencies": {
"hono": "^0.5.1"
},
"devDependencies": {
"esbuild": "^0.14.23",
"miniflare": "2.2.0"
}
Not only for Web API
「家系ラーメン食べたい!」
mustache
ブランチ
serve-static Middleware
mustache Middleware
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
ブランチ
CDNエッジでも「ちゃんとした」Webがつくれる
sw.js
.sw.js
.sw.js
serves sw.js
.sw.js
is registered on /
./sw/*
./server/hello
=> from the server./sw/hello
=> from the browser.おしまい