# anelsonia2

> FP style Node.js web framework.

Latest version **0.7.1** (published 2022-01-26) · MPL-2.0 license · 0 weekly downloads

## Install

```sh
npm install anelsonia2
pnpm add anelsonia2
yarn add anelsonia2
bun add anelsonia2
```

## Health

**Score 20/100 (F)** — status: abandoned.

Positive: has types; no vulnerabilities.

Warnings: low downloads; no esm support; pre 1.0.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 0.7.1 |
| Published | 2022-01-26 |
| First published | 2021-07-20 |
| Weekly downloads | 0 |
| License | MPL-2.0 |
| TypeScript types | bundled |
| Module format | CommonJS |
| Dependencies | 4 |
| Unpacked size | 96.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Author | Hexiang Qi |
| Maintainers | qihexiang |

## Links

- npm: https://www.npmjs.com/package/anelsonia2
- Repository: https://github.com/qihexiang/anelsonia
- Homepage: https://qihexiang.github.io/anelsonia
- Issues: https://github.com/qihexiang/anelsonia/issues
- npm.io page: https://npm.io/package/anelsonia2

## Dependencies (4)

- [mime](https://npm.io/package/mime.md) ^2.5.2
- [destroy](https://npm.io/package/destroy.md) ^1.0.4
- [@types/koa](https://npm.io/package/@types/koa.md) ^2.13.4
- [@types/node](https://npm.io/package/@types/node.md) ^14.14.37

## Recent versions

- 0.7.1 (latest) — 2022-01-26
- 0.7.0 — 2022-01-26
- 0.6.4 — 2022-01-24
- 0.6.3 — 2022-01-21
- 0.6.2 — 2022-01-21
- 0.6.1 — 2022-01-21
- 0.6.0 — 2022-01-20
- 0.5.0 — 2022-01-19
- 0.4.3 — 2022-01-18
- 0.4.2 — 2022-01-15
- 0.4.1 — 2022-01-14
- 0.4.0 — 2022-01-14
- 0.3.5 — 2022-01-14
- 0.3.4 — 2022-01-13
- 0.3.3 — 2022-01-13
- … 31 more at https://npm.io/package/anelsonia2/versions

## README

# Anelsonia2

Anelsonia2是一个Web服务工具组。它包含：

1. 核心组件：`shim`函数，将入口函数转换成`http`，`https`或`http2`模块可以使用的请求监听器。
2. 路由组件：用于创建路由和响应路由的函数
3. 响应组件：快速构建响应体的工具
4. 工具组件：目前包含了一个可以根据HTTP状态码查询对映状态消息的对象

> API Doc: https://qihexiang.github.io/anelsonia

## 快速开始

### 安装

```bash
npm install anelsonia2
```

### 创建入口

需要使用`shim`来将入口函数`EntryPoint`转换为`createServer`或`createSecureServer`的处理器函数的函数，这个函数在`anelsonia2/core`中。

```typescript
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类

```typescript
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中一般会正常显示。

例子：

```js
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`，这需要开发者自行谨慎处理。

下面是一个示例：

```js
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的泛型类型。

方式如下：

```js
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`往往还需要其他参数的输入，你可以这样来获得额外参数：

```ts
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`可以接受一个额外的自定义类型参数；返回的路由匹配时也可以接收一个对应的参数。例如上面的例子会变成：

```ts
// 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`）。

```js
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块。

```js
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`。举例说明：

```ts
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`中填入两个参数：

```ts
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`创建的包裹不会改变原始函数的输入输出。例如，我们有一个这样的核心逻辑函数：

```ts
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;
};
```

要输出一个请求处理用时：

```ts
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`的时长：

```ts
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`的具体类型，若不是，则需要单独声明：

```ts
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异步，最终目标为异步函数，需要明确类型。

---
_Source: https://npm.io/package/anelsonia2 · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
