8track 中文文档教程

发布于 3年前 浏览 28 项目主页 更新于 3年前

8Track - A Service Worker Router

想要:一个更好的标志

带有异步中间件和受 Koa 启发的 neato 类型推断的 service worker 路由器

./doc/img/screen-1.png

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

./doc/img/screen-3.png

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

./doc/img/screen-1.png

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

./doc/img/screen-3.png

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:

./doc/img/screen-2.png

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
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文