8track 中文文档教程
8Track - A Service Worker Router
想要:一个更好的标志
带有异步中间件和受 Koa 启发的 neato 类型推断的 service worker 路由器
Installation
npm install -S 8track
-- Or yarn
yarn add 8track
TypeScript
这个库是用 TypeScript 编写的,所以打字是捆绑在一起的。
Basic usage
import { Router, handle } from '8track'
const router = new Router()
router.all`(.*)`.use(async (ctx, next) => {
console.log(`Handling ${ctx.event.request.method} - ${ctx.url.pathname}`)
await next()
console.log(`${ctx.event.request.method} - ${ctx.url.pathname}`)
})
router.get`/`.handle((ctx) => ctx.html('Hello, world!'))
router.all`(.*)`.handle((ctx) => ctx.end('Not found', { status: 404 }))
addEventListener('fetch', (event) => handle({ event, router }))
Examples
Add CORS headers
import { Router } from '8track'
const router = new Router()
router.all`(.*)`.use(async (ctx, next) => {
const allowedOrigins = ['https://www.myorigin.com']
const allowedHeaders = ['Content-type', 'X-My-Custom-Header']
const allowedMethods = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
ctx.response.headers.append('Vary', 'Origin')
ctx.response.headers.append('Access-Control-Allow-Origin', allowedOrigins.join(','))
ctx.response.headers.append('Access-Control-Allow-Headers', allowedHeaders.join(','))
ctx.response.headers.append('Access-Control-Allow-Methods', allowedMethods.join(','))
ctx.response.headers.append('Access-Control-Allow-Credentials', 'true')
if (ctx.req.method === 'OPTIONS') {
return ctx.end('', { status: 204 })
}
await next()
})
Catch all errors and display error page
import { Router, getErrorPageHTML } from '8track'
const router = new Router()
addEventListener('fetch', (e) => {
const res = router.getResponseForEvent(e).catch(
(error) =>
new Response(getErrorPageHTML(e.request, error), {
status: 500,
headers: {
'Content-Type': 'text/html',
},
}),
)
e.respondWith(res as any)
})
Attach new properties to each request
每个中间件和路由处理程序都会收到一个新的 ctx 对象副本,但是一个特殊的data
属性下的对象是可变的,应该用于在处理程序之间共享数据
interface User {
id: string
name: string
}
// Pretend this is a function that looks up a user by ID
async function getUserById(id: string): Promise<User | undefined> {
return null as any
}
// The describes the shape of the shared data each middleware will use
interface RouteData {
user?: User
}
// This middleware attaches the user associated to the route to the request
const getUserMiddleware: Middleware<RouteData, { userId: string }> = async (ctx, next) => {
ctx.data.user = await getUserById(ctx.params.userId)
await next()
}
const router = new Router<RouteData>()
// For all user requests, attach the user
router.all`/users/${'userId'}`.use(getUserMiddleware)
router.get`/users/${'userId'}`.handle((ctx) => {
if (!ctx.data.user) return ctx.end('Not found', { status: 404 })
ctx.json(JSON.stringify(ctx.data.user))
})
Sub-router mounting
const apiRouter = new Router()
const usersRouter = new Router()
const userBooksRouter = new Router()
usersRouter.get`/`.handle((ctx) => ctx.end('users-list'))
usersRouter.get`/${'id'}`.handle((ctx) => ctx.end(`user: ${ctx.params.id}`))
userBooksRouter.get`/`.handle((ctx) => ctx.end('books-list'))
userBooksRouter.get`/${'id'}`.handle((ctx) => ctx.end(`book: ${ctx.params.id}`))
usersRouter.all`/${'id'}/books`.use(userBooksRouter)
apiRouter.all`/api/users`.use(usersRouter)
API
Router
实例化一个新路由器
const router = new Router<{ logger: typeof console.log }>()
.getResponseForEvent(request: FetchEvent): Promise| undefined
给定一个事件,运行匹配的中间件链并返回链返回的响应。
Router handlers and middleware
与路由器交互的主要方式是通过方法标签添加路由:
router.post`/api/users`.handle((ctx) => ctx.json({ id: 123 }))
在上面的示例中,post
标签返回一个 RouteMatchResult 对象。
Method Matchers
这些方法中的每一个都返回一个 RouteMatchResult 对象。
- .all`pattern`
- .get`pattern`
- .post`pattern`
- .put`pattern`
- .patch`pattern`
- .delete`pattern`
- .head`pattern`
- .options`pattern`
RouteMatchResult
当您在路由器上使用模板标签时,您会创建一个 RouteMatchResult。
router.patch`/api/users/${'id'}` // RouteMatchResult
RouteMatchResult 对象允许您安装仅在模式匹配时运行的路由处理程序或中间件。
router.patch`/api/users/${'id'}`.use(async (ctx, next) => {
console.log('Before: User ID', ctx.params.id)
await next()
console.log('After: User ID', ctx.params.id)
})
.handle((ctx: Context) => any)
安装一个应该返回 Response 实例的路由处理程序
.use((ctx: Context, next: () => Promise) => any)
安装一个可以选择提前终止链并处理请求的路由中间件。
router.patch`/api/users/${'id'}`.use(async (ctx, next) => {
console.log('Before: User ID', ctx.params.id)
if (ctx.params.id === '123') {
return ctx.end(Response.redirect(302))
}
await next()
console.log('After: User ID', ctx.params.id)
})
Context
每个路由处理程序和中间件都会收到一个 Context
实例。
Context Properties
readonly event: FetchEvent
readonly params: Params
response: Response
data: Data
url: URL
Context Methods
end(body: string | ReadableStream | Response | null, responseInit: ResponseInit = {})
html(body: string | ReadableStream, responseInit: ResponseInit = {})
json(body: any, responseInit: ResponseInit = {})
What's up with that weird syntax?
8track 使用名为 tagged templates 的 JavaScript 功能来提取参数名称来自 url 模式。 TypeScript 能够从标记的模板文字中提取类型:
const bar = 123
const baz = new Date()
// Extracted type here is a tuple [number, Date]
foo`testing: ${bar} - ${baz} cool`
// But things get interesting when using literal types
// Extracted type here is a tuple ['bar', 'baz']
foo`testing: ${'bar'} - ${'baz'} cool`
由于模板文字能够提取类型为传入的文字值的元组,我们可以利用泛型来描述路由参数的形状:
< img src="./doc/img/screen-2.png" alt="./doc/img/screen-2.png">
Built-in Middleware
KV Static
提供来自 Cloudflare KV 的文件。
import { Router, kvStatic } from '8track'
const router = new Router()
router.all`(.*)`.use(kvStatic({ kv: myKvNamespaceVar, maxAge: 24 * 60 * 60 * 30 }))
Deploying your worker
8track 带有一个 CLI 来上传你的工作人员并同步你的 kv 文件。 为了使用 8track 的 kv 静态文件中间件,您必须使用此 CLI 上传您的文件。
将部署脚本添加到您的 package.json:
{
"scripts": {
"deploy": "8track deploy --worker dist/worker.js --kv-files dist/client.js,dist/client.css"
}
}
注意:这还不支持 glob!
您需要设置以下环境变量:
# Your Cloudflare API Token
CF_KEY
# Your Cloudflare account email
CF_EMAIL
# Your Cloudflare account ID
CF_ID
# The ID of the namespace
KV_NAMESPACE_ID
# The name of the KV namespace you want to use
KV_NAMESPACE
# The variable name your KV namespace is bound to
KV_VAR_NAME
8Track - A Service Worker Router
Wanted: a better logo
A service worker router with async middleware and neato type-inference inspired by Koa
Installation
npm install -S 8track
-- Or yarn
yarn add 8track
TypeScript
This library is written in TypeScript, so typings are bundled.
Basic usage
import { Router, handle } from '8track'
const router = new Router()
router.all`(.*)`.use(async (ctx, next) => {
console.log(`Handling ${ctx.event.request.method} - ${ctx.url.pathname}`)
await next()
console.log(`${ctx.event.request.method} - ${ctx.url.pathname}`)
})
router.get`/`.handle((ctx) => ctx.html('Hello, world!'))
router.all`(.*)`.handle((ctx) => ctx.end('Not found', { status: 404 }))
addEventListener('fetch', (event) => handle({ event, router }))
Examples
Add CORS headers
import { Router } from '8track'
const router = new Router()
router.all`(.*)`.use(async (ctx, next) => {
const allowedOrigins = ['https://www.myorigin.com']
const allowedHeaders = ['Content-type', 'X-My-Custom-Header']
const allowedMethods = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
ctx.response.headers.append('Vary', 'Origin')
ctx.response.headers.append('Access-Control-Allow-Origin', allowedOrigins.join(','))
ctx.response.headers.append('Access-Control-Allow-Headers', allowedHeaders.join(','))
ctx.response.headers.append('Access-Control-Allow-Methods', allowedMethods.join(','))
ctx.response.headers.append('Access-Control-Allow-Credentials', 'true')
if (ctx.req.method === 'OPTIONS') {
return ctx.end('', { status: 204 })
}
await next()
})
Catch all errors and display error page
import { Router, getErrorPageHTML } from '8track'
const router = new Router()
addEventListener('fetch', (e) => {
const res = router.getResponseForEvent(e).catch(
(error) =>
new Response(getErrorPageHTML(e.request, error), {
status: 500,
headers: {
'Content-Type': 'text/html',
},
}),
)
e.respondWith(res as any)
})
Attach new properties to each request
Each Middleware and route handler receives a new copy of the ctx object, but a special object under the data
property is mutable and should be used to share data between handlers
interface User {
id: string
name: string
}
// Pretend this is a function that looks up a user by ID
async function getUserById(id: string): Promise<User | undefined> {
return null as any
}
// The describes the shape of the shared data each middleware will use
interface RouteData {
user?: User
}
// This middleware attaches the user associated to the route to the request
const getUserMiddleware: Middleware<RouteData, { userId: string }> = async (ctx, next) => {
ctx.data.user = await getUserById(ctx.params.userId)
await next()
}
const router = new Router<RouteData>()
// For all user requests, attach the user
router.all`/users/${'userId'}`.use(getUserMiddleware)
router.get`/users/${'userId'}`.handle((ctx) => {
if (!ctx.data.user) return ctx.end('Not found', { status: 404 })
ctx.json(JSON.stringify(ctx.data.user))
})
Sub-router mounting
const apiRouter = new Router()
const usersRouter = new Router()
const userBooksRouter = new Router()
usersRouter.get`/`.handle((ctx) => ctx.end('users-list'))
usersRouter.get`/${'id'}`.handle((ctx) => ctx.end(`user: ${ctx.params.id}`))
userBooksRouter.get`/`.handle((ctx) => ctx.end('books-list'))
userBooksRouter.get`/${'id'}`.handle((ctx) => ctx.end(`book: ${ctx.params.id}`))
usersRouter.all`/${'id'}/books`.use(userBooksRouter)
apiRouter.all`/api/users`.use(usersRouter)
API
Router
Instantiate a new router
const router = new Router<{ logger: typeof console.log }>()
.getResponseForEvent(request: FetchEvent): Promise| undefined
Given an event, run the matching middleware chain and return the response returned by the chain.
Router handlers and middleware
The primary way to interact with the router is to add routes via method tags:
router.post`/api/users`.handle((ctx) => ctx.json({ id: 123 }))
In the above example, the post
tag returns a RouteMatchResult object.
Method Matchers
Each of these methods returns a RouteMatchResult object.
- .all`pattern`
- .get`pattern`
- .post`pattern`
- .put`pattern`
- .patch`pattern`
- .delete`pattern`
- .head`pattern`
- .options`pattern`
RouteMatchResult
When you use a template tag on the router, you create a RouteMatchResult.
router.patch`/api/users/${'id'}` // RouteMatchResult
The RouteMatchResult object allows you to mount a route handler or a middleware that only runs when the pattern is matched.
router.patch`/api/users/${'id'}`.use(async (ctx, next) => {
console.log('Before: User ID', ctx.params.id)
await next()
console.log('After: User ID', ctx.params.id)
})
.handle((ctx: Context) => any)
Mount a route handler that should return an instance of Response
.use((ctx: Context, next: () => Promise) => any)
Mount a route middleware that can optionally terminate the chain early and handle the request.
router.patch`/api/users/${'id'}`.use(async (ctx, next) => {
console.log('Before: User ID', ctx.params.id)
if (ctx.params.id === '123') {
return ctx.end(Response.redirect(302))
}
await next()
console.log('After: User ID', ctx.params.id)
})
Context
Each route handler and middleware receives an instance of Context
.
Context Properties
readonly event: FetchEvent
readonly params: Params
response: Response
data: Data
url: URL
Context Methods
end(body: string | ReadableStream | Response | null, responseInit: ResponseInit = {})
html(body: string | ReadableStream, responseInit: ResponseInit = {})
json(body: any, responseInit: ResponseInit = {})
What's up with that weird syntax?
8track uses a JavaScript feature called tagged templates in order to extract parameter names from url patterns. TypeScript is able extract types from tagged template literals:
const bar = 123
const baz = new Date()
// Extracted type here is a tuple [number, Date]
foo`testing: ${bar} - ${baz} cool`
// But things get interesting when using literal types
// Extracted type here is a tuple ['bar', 'baz']
foo`testing: ${'bar'} - ${'baz'} cool`
Since the template literal is able to extract a tuple whose types are the literal values passed in, we can utilize generics to describe the shape of the route parameters:
Built-in Middleware
KV Static
Serves files from Cloudflare KV.
import { Router, kvStatic } from '8track'
const router = new Router()
router.all`(.*)`.use(kvStatic({ kv: myKvNamespaceVar, maxAge: 24 * 60 * 60 * 30 }))
Deploying your worker
8track comes with a CLI to upload your worker and sync your kv files. In order to use 8track's kv static file middleware, you must upload your files using this CLI.
Add a deploy script to your package.json:
{
"scripts": {
"deploy": "8track deploy --worker dist/worker.js --kv-files dist/client.js,dist/client.css"
}
}
Note: This does not support globs yet!
You'll need the following environment variables set:
# Your Cloudflare API Token
CF_KEY
# Your Cloudflare account email
CF_EMAIL
# Your Cloudflare account ID
CF_ID
# The ID of the namespace
KV_NAMESPACE_ID
# The name of the KV namespace you want to use
KV_NAMESPACE
# The variable name your KV namespace is bound to
KV_VAR_NAME