0.4.0 • Published 3 years ago

anelsonia v0.4.0

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

Anelsonia

一个更加简单、静态的Node.js Web框(玩)架(具)。

为什么?

Node.js的基础Web框架已经有很多,如最为著名的Express、Koa、Fastify等,这些框架拥有良好的生态,建立起了一系列可靠的应用,但是我不喜欢它们——

自Express框架开始,这一系列洋葱模型的框架有效的利用了JavaScript的动态属性,在运行时将一系列属性绑定到它们的 reqres 或是 ctx 属性上,直到服务启动之前——不,更准确的来说,当请求进入到你所设定的路径之前,你是无法知道动态绑定的属性和功能是否正常工作的。这对于TypeScript开发来说其实挺令人烦恼。

除此之外,路由这一需求实际上也和洋葱模型格格不入:一旦存在路由分支,各个路由之间的逻辑关系显然就不再“洋葱”了。而就算没有这些路由,洋葱模型也并不是真的很实用,相当数量的中间件:例如响应内容压缩、请求内容解析,都并不是真的需要在进站和出站时都要进行的,但是因为框架是洋葱模型的,因此不得不设计成洋葱中的一层。

不管上面的理由是否真的有道理,总之我认为一个HTTP请求的处理过程,是可以用函数式风格的代码来表达的,因此做了这个框架。

当然啦,我很菜,所以这个框架可能既不OOP也不FP,feature少bug多,还请见谅啦。

我又觉得我行了。

hello, world

处理请求的入口函数,应该实现接口 EntryPoint ,你可以在编辑器中先引入这个接口来帮助你完成这个函数:

import { EntryPoint } from "anelsonia"
const entry: EntryPoint = (req) => {
    return {
        statusCode: 200,
        statusMessage: "Ok",
        headers: { "Content-Type": "text/plain" },
        data: "hello, world\n"
    }
}

这个函数只有一个参数,即 req,它是 http 模块的 IncomingMessage 接口的实现;函数的返回值是接口 ResponseBody 的实现,它的定义如下:

interface ResponseBody {
    statusCode: number;
    statusMessage: string;
    headers: OutgoingHttpHeaders;
    data: string | Buffer | Readable;
}

要更为简化的定义 statusMessage 属性,可以使用本框架导出的 HttpStatus 对象来获取对应的默认消息,例如 HttpStatus[200] 的值是 'Ok'HttpStatus[404] 的值是 'Not found'

不过,这样的返回方式依然较为麻烦,因此框架中提供了一些快速生成状态为 200 ,消息为 Ok,并在 headers 中设置正确的 "Content-Type"ResponseBody的函数。

这些函数,可以从 resBuilder 中获得,例如上面的案例,可以变化为:

import { EntryPoint, resBuilder } from "anelsonia"

const entry: EntryPoint = (req) => {
    return resBuilder.text("hello, world\n")
}

要建立一个完整的服务,需要建立一个服务器,可以写成如下样式:

import { createServer, EntryPoint, resBuilder } from "anelsonia"

const entry: EntryPoint = (req) => {
    return resBuilder.text("hello, world\n")
}

const server = createServer(entry)
server.listen(8080)

注意,server 实际上是一个标准的 http 模块的 Server 实例。

要更加细致的控制建立流程,你可以利用 genBaseHandlerEntryPoint 实例转化为 RequestHandler 实例,然后传入服务器:

import { createServer } from "http"
import { genBaseHandler, EntryPoint, resBuilder } from "anelsonia"

const entry: EntryPoint = (req) => {
    return resBuilder.text("hello, world\n")
}

const httpHandler = genBaseHandler(entry)

const server = createServer(httpHandler)
server.listen(8080)

路由

使用 createRouter 函数构建一个路由,使用路由返回的 routeMatcher 可以传入 url 变量用于匹配和解析,传入的 handlerRouteHandler 的实现用于处理对应路由下的操作。 routeMatcher 的返回值取决于 handler 的返回值,例如下面的案例中匹配上的路由会返回 ResponseBodyPromise<ResponseBody>,没有的则返回 null

import { createServer, EntryPoint, resBuilder, createRouter, RouteHandler, ResponseBody } from "anelsonia";
import { errorHandler } from "./errorHandler";
import { fileHandler } from "./fileHandler";
import { helloHandler } from "./helloHandler";

const helloRoute = createRouter("/hello/:username");
const fileRouter = createRouter("/public/(.*)");
const fileFallbackRouter = createRouter("/public")
const errorTest = createRouter("/error/:errCode");

const entry: EntryPoint = async (req) => {
    const url = req.url ?? "/";
    return helloRoute(url, (p, q) => helloHandler(p, q, req))
        || fileRouter(url, fileHandler)
        || fileFallbackRouter(url, (p, q) => {
            return resBuilder.redirection(
                302, "http://" + req.headers.host + "/public/", ""
            )
        })
        || errorTest(url, errorHandler)
        || resBuilder.httpError(404);
};

createServer(entry).listen(8080);

匹配语法

因为使用了和Express一样的 path-to-regexp 库,因此可以使用完全一样的方式构建路由。

RouteHandler和路由参数

pathParams 参数是路由参数,它是一个 Map 对象,使用 get 方法从其中获取值(字符串),利用 forEach 遍历键值对, searchParams 是查询参数,它是一个 URLSearchParams 实现,它和 Map 对象的用法基本一致。

如果想要直接返回 pathParamssearchParams 而不经过特别的 RouteHandler,可以使用导出的 getParams 作为 handler 参数的值。

RouteHndler 并不一定要返回一个 ResponseBody 的实现,也可以返回任意值作为一个处理过程的中间量。

RouteHandler 可以接受超过两个参数,可以使用箭头函数来进行传参,如上面的 helloHandler ,它对应的定义如下:

import { resBuilder, ResponseBody, RouteHandler } from "anelsonia";
import { IncomingMessage } from "http";
import getRawBody from "raw-body";

export const helloHandler: RouteHandler<ResponseBody | null> = async (p, q, req: IncomingMessage) => {
    const username = p.get("username");
    const message = q.get("message");
    if (req.method == "post") console.log((await getRawBody(req)).toString());
    if (!(username && message)) {
        return null;
    } else {
        return resBuilder.json({ username, message, date: new Date() });
    }
};

特别需要注意的是,路由控制器和入口函数都可以是异步函数,且特别建议将入口函数设置为异步函数,以使用 await 处理后续的异步动作;于此同时,需要注意 Promise<null> 在使用 || 进行向后传递时,会被视为 true 的结果,如果想要以此方法在进入 RouteHandler 后再逃出路由时必须 await

resBuilder

resBuilder对象下有以下函数,可返回 ResponseBody 的实现:

名称参数说明
textcontent: stirng, type: string文本内容相应,指定的类型会被设置为"Content-Type": "text/${type}",type留空时,默认值为"plain"
htmlcontent: string响应一个HTML,Content-Type与之对应
css同上响应一个CSS,Content-Type与之对应
js同上响应一个JavaScript,Content-Type与之对应
jsonobject: Object响应一个JSON,响应前会被JSON.stringify转换成字符串
streamrStream: Readable返回一个可读流,当其 "data" 事件触发时,会写入到响应流中
bufferbuf: Buffer直接返回Buffer二进制内容,默认的Content-Type和stream方法一样是application/octect-stream
filepath: string构建一个文件读取流,调用stream函数,Content-Type由 mime.getType 确定,未知的内容为 application/octect-stream
httpErrorcode: number, message?: string返回一个HTTP错误,可以指定状态码和错误信息

注意,因为处理流时对于出错的情况,我是瞎几把处理的(其实就是没处理),不管是手动实现 ResponseBody 还是使用我的 streamfile 函数都一样。所以使用前请务必小心并多加测试。

建议插件列表

大部分可能用到的插件实际上是Express/Koa的依赖或者依赖的依赖。

目的插件名称
Cookie解析cookie
Body解析raw-body
设置Content-Typemime
设置Content-Typecontent-type
0.4.0

3 years ago

0.3.4

3 years ago

0.3.3

3 years ago

0.3.2

3 years ago

0.3.0

3 years ago

0.3.1

3 years ago

0.2.4

3 years ago

0.2.3

3 years ago

0.2.2

3 years ago

0.1.11

3 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.10

3 years ago

0.1.9

3 years ago

0.1.8

3 years ago

0.1.7

3 years ago

0.1.4

3 years ago

0.1.6

3 years ago

0.1.5

3 years ago

0.1.2

3 years ago

0.1.3

3 years ago

0.1.0

3 years ago

0.1.1

3 years ago

0.0.3

3 years ago

0.0.4

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago