anelsonia2 v0.7.1
Anelsonia2
Anelsonia2是一个Web服务工具组。它包含:
- 核心组件:
shim
函数,将入口函数转换成http
,https
或http2
模块可以使用的请求监听器。 - 路由组件:用于创建路由和响应路由的函数
- 响应组件:快速构建响应体的工具
- 工具组件:目前包含了一个可以根据HTTP状态码查询对映状态消息的对象
快速开始
安装
npm install anelsonia2
创建入口
需要使用shim
来将入口函数EntryPoint
转换为createServer
或createSecureServer
的处理器函数的函数,这个函数在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: 设置响应头
需要注意的事情有:
- 除了
setHeaders
,对已有的Respond
对象使用设置方法,会替换掉对象内已有的属性,而setHeaders
会合并属性 setHeaders
可以接受多个参数,他们会被合并,后面的参数具有更高的优先级- 未设置状态码时和响应主体时,默认的状态码是404,设置了响应主体后,默认的状态码是200
- 未设置状态消息时,它的值会从状态码中推断出来。
- 未设置响应主体时,它的值和状态消息相同,响应头会被加入
{"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>/ | null | null | null |
/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
),因此当你需要返回null
或undefined
时,必须将其包裹起来,例如{ 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
来创建交换机。
同时创建路由和交换机
利用createRoute
和createSwitcher
,我们可以在分离的多个地方对路由规则进行定义,但与此同时,也会有人更倾向与将路径匹配模式集中定义于一处,此时重复使用createRoute
和createSwitcher
就显得十分麻烦。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
函数参数类型一致,返回中解析出的switcher
和createSwitcher
的类型是一致的。
另外,提供了一个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
请求分为一组,POST
和PUT
请求分为一组。调用链中,match
的一个参数是字符串或字符串数组,当字符串和分流依据相等,或数组中存在匹配的字符串时,或给定的正则表达式与分流依据匹配时,会执行后续的handler
,所有注册的handler
应该有相同的返回类型或符合condition<T>
描述的泛型。解构出的result
是handler
的返回值。
注意,由于此处做了变量解构,不能在
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
createWrapper
和createEffect
类似,区别在于它的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;
}]
);
createWrapper
的hook
参数接收目标函数需要的参数,返回一个二元组:
- 第一个元素是原始函数的参数,用数组的形式提供;
- 第二个元素是一个函数,在原始函数执行后,接受其返回值执行,并返回目标函数的返回值。
如果目标函数和原始函数类型一致,则无需声明泛型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异步,最终目标为异步函数,需要明确类型。
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago