npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

anelsonia2

v0.7.1

Published

FP style Node.js web framework.

Readme

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>/|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),因此当你需要返回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异步,最终目标为异步函数,需要明确类型。