0.7.1 • Published 2 years ago

anelsonia2 v0.7.1

Weekly downloads
-
License
MPL-2.0
Repository
github
Last release
2 years ago

Anelsonia2

Anelsonia2是一个Web服务工具组。它包含:

  1. 核心组件:shim函数,将入口函数转换成httphttpshttp2模块可以使用的请求监听器。
  2. 路由组件:用于创建路由和响应路由的函数
  3. 响应组件:快速构建响应体的工具
  4. 工具组件:目前包含了一个可以根据HTTP状态码查询对映状态消息的对象

API Doc: https://qihexiang.github.io/anelsonia

快速开始

安装

npm install anelsonia2

创建入口

需要使用shim来将入口函数EntryPoint转换为createServercreateSecureServer的处理器函数的函数,这个函数在anelsonia2/core中。

import { shim } from "anelsonia2"
import { createServer } from "http"
import { createSecureServer } from "http2"

const reqHandler = shim(async req => {
    return {
        statusCode: 200, statusMessage: "Ok", body: "nice to meet you",
        headers: {"content-type": "text/plain"}
    }
})

createServer(reqHandler).listen(8080)
createServer(http2Options, reqHandler).listen(8081)

这里对HTTP/2的支持是不完整的,无法自行操作整个会话的Socket,要充分使用HTTP/2的功能,应该直接使用http2模块进行编程而不是使用anelsonia2来处理请求,但路由、响应构建工具仍然可以单独使用。

响应工具

上面我们已经构建了一个会返回nice to meet you消息的服务器了,但是手动构建一个返回对象是麻烦的,所以我们使用工具组提供的响应工具来构建它。

Respond类

import { shim, Respond } from "anelsonia2"

const reqHandler = shim(async req => {
    return new Responsd()
        .setBody("nice to meet you")
        .setHeaders({"content-type": "text/plain"})
})

这样,状态码和状态消息都自动设置了。

响应工具是一个名为Respond的类。

提供了四个getter来返回对应的值,它们实现了入口函数需要返回的ResponseProps

  • statusCode: 有效的HTTP状态码
  • statusMessage: 字符串类型的状态消息
  • body: 字符串或Buffer或可读流的响应主体
  • setHeaders: 响应头

它的构造函数没有参数,但提供四个方法来设置上述四项属性,他们的参数和属性的类型一致:

  • setStatusCode: 设置状态码
  • setStatusMessage: 设置状态消息
  • setBody: 设置响应主体
  • setHeaders: 设置响应头

需要注意的事情有:

  1. 除了setHeaders,对已有的Respond对象使用设置方法,会替换掉对象内已有的属性,而setHeaders会合并属性
  2. setHeaders可以接受多个参数,他们会被合并,后面的参数具有更高的优先级
  3. 未设置状态码时和响应主体时,默认的状态码是404,设置了响应主体后,默认的状态码是200
  4. 未设置状态消息时,它的值会从状态码中推断出来。
  5. 未设置响应主体时,它的值和状态消息相同,响应头会被加入{"content-type": "text/plain"}

Respond::create和createRes

当然,更多的时候我们希望用一个函数就能够构建好ResponseProps,此处也提供了对Respond类的额外创建方法:Respond.create()函数,它也使用createRes的名称导出,且是anelsonia2/core/response的默认导出。该函数提供了7个重载,请参见API文档中的相关内容:https://qihexiang.github.io/anelsonia/classes/Respond.html#create

该函数提供的多种重载能让人较为舒适地创建Respond

API文档无法自动地为createRes转发函数注释文档,但是在IDE中一般会正常显示。

例子:

import { createRes } from "anelsonia2"

createRes() // 404 response
createRes("hello, world") // response body
createRes(500) // status code
create(302, "/login") // status code and body
create(200, {"Server": "anelsonia2"}) // status code and headers
create("hello, world", {"Content-Type": "text/plain charset=UTF-8"}) // body and headers
createRes(206, partialStream, headers) // status code, body and headers

路由

由于函数式的设计,这个库并不包含像express那样的app.get(pattern, handler)风格的路由,而是通过工具函数来实现请求路径的区分的。

路由(Route)

路由在Anelsonia中的概念是,当用户访问的路径符合某一个规则的时候,则执行对应的函数,并获得函数的返回值。要创建一个路由,使用createRoute函数来实现。

createRoute中可以传入两个参数,第一个是路由匹配模式pattern,格式像这样:/user/<username>/<age>/,这样,就可以匹配到类似于/user/freesia/16/这样的路径。

表明路径参数的形式有三种:

  • <T>:非贪婪模式,匹配任意字符出现至少一次。
  • <[T]>:贪婪模式,匹配任意字符至少出现一次。
  • [T]:贪婪模式,匹配任意字符,也可以没有字符。

可以观看这个例子中,对filepath的捕获情况来理解:

模式/路径/user/hx/docs/index.md/user/hx/docs/index.md//user/hx/
/user/<name>/<filepath>/nullnullnull
/user/<name>/<filepath>"docs/index.md""docs/index.md/"null
/user/<name>/<[filepath]>/null"doc/index.md"null
/user/<name>/[filepath]"docs/index.md""docs/index.md/"""

另一个参数自然是对应的函数handler,这个函数有2个参数,其一是根据pattern推导出的路由匹配参数params,例如上面的例子中,推导出的参数类型为{name: string, filepath: string},所有的路由参数类型都是string,开发者应该根据实际的情况进行检查和类型转换。另一个参数是搜索参数quries,它的类型是UrlSeachParams,由于路由匹配并不检查搜索参数的合法性,因此并不进行类型标注,开发者在使用时应当注意到quries.get方法取回的值可能为null,这需要开发者自行谨慎处理。

下面是一个示例:

import { createRoute } from "anelsonia2" 

const route = createRoute('/user/<username>/<filepath>', async ({username, filepath}, queries) 
    => JSON.stringify(await readDir(username, filepath)))
const result = await route(url)

交换机(Switcher)

每个Route只是一条路径,实际上需要使用多条路径进行依次匹配。使用交换机(Switcher)实现该功能。例如有路由route1-route6,他们的handler拥有相同的返回类型,则可以聚合在一起。

返回类型不同的平级路由,使用联合类型作为Switcher的泛型类型。

方式如下:

import { createSwitcher } from "anelsonia2"
const switcher = createSwitcher(route1, route2, route3, route4, route5, route6)
const result = switcher(url)

Switcher最终得到的函数和Route实际上是一样的,因此可以逐级将多个Switcher也聚合起来。

注意,由于switcher的实现使用了??运算符(判断匹配失败的依据是路由返回null),因此当你需要返回nullundefined时,必须将其包裹起来,例如{ value: null }

额外参数

在实际使用中,handler往往还需要其他参数的输入,你可以这样来获得额外参数:

import apiRouteHandler from "./controller/api"
import DB from "./data/IO"
const db = new DB();

function main(req: Request) {
    const result = await createRoute("/api/<options>", ({options}, queries) => apiRouteHandler(options, queries, {req, db}))(req.url)
    return result
}

但如果我们想将路径和控制器绑定之后再接收参数的话,就无法做到了。因此,本库中提供了额外的函数:createExtendRoute。它的使用与createRoute基本相同,差异在于它的handler可以接受一个额外的自定义类型参数;返回的路由匹配时也可以接收一个对应的参数。例如上面的例子会变成:

// controller/api.ts
export const apiRoute = createExtendRoute('/api/<options>', ({options}, queries, {req: Request, db: DB}) => {...})

// main.ts
import { apiRoute } from "./controller/api"
import DB from "./data/IO"
const db = new DB();

function main(req: Request) {
    const route = await apiRoute(req.url, {req, db})
    return result
}

若有多个这样的扩展路由进行聚合时,可以使用createExtendSwitcher来创建交换机。

同时创建路由和交换机

利用createRoutecreateSwitcher,我们可以在分离的多个地方对路由规则进行定义,但与此同时,也会有人更倾向与将路径匹配模式集中定义于一处,此时重复使用createRoutecreateSwitcher就显得十分麻烦。Anelsonia提供了一个额外的函数createSwRt来实现这个功能,他提供一个链试调用来创建一个交换机及其对应的路由。

例子如下,有若干已经定义好的handler,其中一些返回是异步的(Promise)。

import { createSwRt } from "anelsonia2"

async function main(req) {
    const { switcher } = createSwRt()
        .route("/user/<username>/<rest>", infoHandler)
        .route("/view/<rest>", viewHandler)
        .route("/d/<username>/<filepath>", downloadHandler)
        .route("/b/<username>/<[filepath]>/", browseHandler)
    const response = (await switcher(req.url)) ?? new Respond().setStatus(404)
    return response
}

每次链式调用中的route函数和createRoute函数参数类型一致,返回中解析出的switchercreateSwitcher的类型是一致的。

另外,提供了一个createExtendSwRt函数,使用方法基本一致。

简单匹配(Condition)

上述的路由匹配模式只能支持URL的路径匹配,但实际上我们还会根据一些具体的情况,例如请求的方法、可枚举的具体路径参数等进行请求分流,这些情况一般需要精确匹配字符串。这使用上面的函数并不容易实现,或者显得更加麻烦,因此提供了一个condition函数,以链试调用的方式来实现类似与switch语法的功能,可以看作是一个带有返回值的switch块。

import { condition } from "anelsonia2"

const { result } = condition(req.method)
    .match('GET', () => getSw(req.url))
    .match(['POST','PUT'], () => uploadSw(req.url, req))
    .match(/^(OPTION|TRACE)$/, (method) => debugSw(req.url, method))

例如,这个例子分流的依据是req.method,我们将GET请求分为一组,POSTPUT请求分为一组。调用链中,match的一个参数是字符串或字符串数组,当字符串和分流依据相等,或数组中存在匹配的字符串时,或给定的正则表达式与分流依据匹配时,会执行后续的handler,所有注册的handler应该有相同的返回类型或符合condition<T>描述的泛型。解构出的resulthandler的返回值。

注意,由于此处做了变量解构,不能在condition前直接使用await来处理异步的result,你需要在之后使用result是写作await result

composeFn函数组合器

提供了一个用于组合函数的组合函数composFn。举例说明:

import { composeFn } from "anelsonia2";

const { fn } = composeFn((x: number) => x + 1)
    .next(x => Math.pow(x, 2))
    .next(x => `The final value is ${x}`);

console.log(fn(2)); // The final value is 9

执行的顺序是显而易见的,前一个函数的计算结果是后一个函数的入参。

你使用的IDE可能会提醒你,这个函数还有一个额外的重载,你可以在composeFn中填入两个参数:

import { composeFn } from "anelsonia2";

const { fn } = composeFn(x => Math.pow(x, 2), (x: number) => x + 1)
    .next(x => `The final value is ${x}`);

console.log(fn(2)); // The final value is 9

你可以注意到第二个函数需要类型标注而第一个并不需要,这是因为composeFn的第一个参数是第二个执行的函数,第二个参数是第一个执行的函数。这个怪异的设计是为了composeFn实现的简便设计的,这个重载也仅仅是为了内部使用,在一般情况下,你不应该使用它。

createEffect附加钩子

在Koa和Express中,洋葱模型和中间件是非常重要的概念,其作用在于提供了简便的请求前处理和后处理方式。作为一个函数库,Anelsonia2不能直接提供这样的功能,但是提供了一种创建函数包装的方式。

第一种方式是createEffect,它用于在函数执行前后产生副作用。createEffect创建的包裹不会改变原始函数的输入输出。例如,我们有一个这样的核心逻辑函数:

const main = async (req: HttpReq, body?: Buffer) => {
    const { switcher } = createSwRt<AsyncResponse>()
        .route("/hello/<yourname>/", helloWorld)
        .route("/list/[dirpath]", ls)
        .route("/download/[filepath]", (p, q) => download(p, q, req))
        .route("/data/", () => {
            console.log(body?.toString());
            return createRes(200);
        });
    const response = await switcher(req.url ?? "/") ?? createRes(404, "No route matched.");
    return response;
};

要输出一个请求处理用时:

const timeMeasure = createEffect<typeof main>(
    req => {
        const start = new Date();
        return async res => {
            console.log(
                `${start.toLocaleString()} ${req.method} ${req.url} ${(await res).statusCode} ${new Date().getTime() - start.getTime()}ms`
            );
        };
    }
);

const newFn = timeMeasure(main)

newFn就是包装后的函数。

我们在createEffect时,制定了被包裹函数的类型作为实例化泛型,然后传入了hook参数,这个函数在被包裹函数执行前运行,称为beforeEffect,返回一个函数,在被包裹函数执行后执行,称为afterEffect

beforeEffect必须是同步的(返回一个可以立即执行的函数),而afterEffect可以是异步的。

createEffect设计上不让两个Effect函数修改输入输出,但是基于动态语言的特性,其实你可以做到这一点,但这是不推荐的。createWrapper函数是解决这一问题的正解。

createWrapper

createWrappercreateEffect类似,区别在于它的beforeHook可以改变原始函数的入参,afterHook会改变原始函数的结果。

createWrapper<O, T>有两个泛型参数,参数O是原始函数的类型,参数N是包装后目标函数的类型,T的默认值是O

其中一个例子是,为所有的请求改变Keep-Alive的时长:

import { createHooks } from "anelsonia2";

const keepAlive = (timeout: number) => createHooks<typeof main>(
    req => [[req], async res => {
        const response = await res;
        if (response instanceof Respond) response.setHeaders({ "Keep-Alive": `timeout=${timeout}` });
        else response.headers = { ...response.headers, "Keep-Alive": `timeout=${timeout}` };
        return response;
    }]
);

createWrapperhook参数接收目标函数需要的参数,返回一个二元组:

  • 第一个元素是原始函数的参数,用数组的形式提供;
  • 第二个元素是一个函数,在原始函数执行后,接受其返回值执行,并返回目标函数的返回值。

如果目标函数和原始函数类型一致,则无需声明泛型N的具体类型,若不是,则需要单独声明:

const square = createWrapper<typeof Math.pow, (x: number) => string>(
    x => [[x, 2], String]
)(Math.pow);

const result = square(2);

console.log(typeof result, result); // -> string 4

在实际使用中,同步和异步是很重要的,我们以函数返回值是否为Promise对象来区分函数是否是异步函数。在createWrapper中,输入的共有三个函数:beforeHook,afterHook和原始函数。当其中任意一个函数为异步函数时,目标函数就是异步函数。

createWrapper<O, T>中,T泛型的默认值是O,这对于多数情况是适用的,但是在存在异步的情况下,我们可能必须声明T的具体类型:

  • 原始函数同步,beforeHook异步,最终目标函数为异步函数,需要明确类型;
  • 原始函数同步,afterHook异步,最终目标为异步函数,需要明确类型。
0.1.0

2 years ago

0.3.0

2 years ago

0.7.1

2 years ago

0.3.5

2 years ago

0.5.0

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.7.0

2 years ago

0.3.4

2 years ago

0.3.3

2 years ago

0.2.0

2 years ago

0.6.3

2 years ago

0.6.2

2 years ago

0.6.4

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.6.1

2 years ago

0.4.3

2 years ago

0.6.0

2 years ago

0.4.2

2 years ago

0.0.27

3 years ago

0.0.28

3 years ago

0.0.29

3 years ago

0.0.22

3 years ago

0.0.23

3 years ago

0.0.24

3 years ago

0.0.25

3 years ago

0.0.26

3 years ago

0.0.20

3 years ago

0.0.21

3 years ago

0.0.11

3 years ago

0.0.12

3 years ago

0.0.13

3 years ago

0.0.14

3 years ago

0.0.15

3 years ago

0.0.16

3 years ago

0.0.17

3 years ago

0.0.18

3 years ago

0.0.19

3 years ago

0.0.10

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago

0.0.0

3 years ago