📖 tl;dr: Request/Response-API 不仅更快,而且使编写测试更容易。

如果你曾经访问过 Node 的主页,你可能见过这段代码片段:

import { createServer } from "node:http";

const server = createServer((req, res) => {
	res.writeHead(200, { "Content-Type": "text/plain" });
	res.end("Hello World!\n");
});

// starts a simple http server locally on port 3000
// 在本地 3000 端口启动一个简单的 http 服务器
server.listen(3000, "127.0.0.1", () => {
	console.log("Listening on 127.0.0.1:3000");
});

它展示了如何创建一个纯粹的 web 服务器并用纯文本进行响应。这是 Node 成名的 API,在它刚出现时具有一定的革命性。如今,这个调用可能已经被框架抽象化了,但在底层,它们都在做着同样的事情。

这很好,它能工作,并且围绕它形成了一个完整的生态系统。然而,并非一切都是美好的。这个 API 一个相当大的缺点是,你必须绑定一个 socket 并实际启动服务器。对于生产环境的使用,这完全没问题,但是当涉及到测试或互操作性时,很快就会变成一种麻烦。当然,对于测试,我们可以像 supertest 那样抽象出所有的样板代码,但即便如此,它也无法绕过必须绑定到实际 socket 的问题。

// Example supertest usage
// Supertest 用法示例
request(app)
	.get("/user")
	.expect(200)
	.end(function (err, res) {
		if (err) throw err;
	});

如果有一个更好的 API,不需要绑定到 socket 呢?

Request, Response 和 Fetch

进入现代时代,使用你可能已经从浏览器中熟悉的 fetch-API。

const response = await fetch("https://example.com");
const html = await response.text();

fetch()-调用返回一个标准的 JavaScript 类实例。但关键是 request 也是如此!因此,如果我们稍微放大来看,这个 API 允许将每个服务器表示为一个函数,该函数接受一个 Request 实例并返回一个 Response

type MyApp = (req: Request) => Promise<Response>;

这反过来意味着你 不再 需要绑定 socket 了!你实际上可以直接创建一个新的 Request 对象并使用它调用你的应用。让我们看看它是什么样子的。这是我们的应用(我知道它很无聊!):

const myApp = async (req: Request) => {
	return new Response("Hello world!");
};

...这是我们如何测试它的:

// No fetch testing library needed
// 不需要 fetch 测试库
const response = await myApp(new Request("http://localhost/"));
const text = await response.text();
expect(text).toEqual("Hello world!");

这里没有涉及到 socket 或任何类似的东西。我们甚至不需要额外的库。这只是纯粹的、普通的 JavaScript。你创建一个对象并将其传递给一个函数。仅此而已。那么,绑定到 socket 最终会产生多少开销呢?

Benchmarktime/iter (avg)iter/s
Node-API806.5 µs1,240
Request/Response-API3.0 µs329,300

嗯,事实证明相当多!这个基准测试可能使差异看起来很小,但是当假设我们有一个测试套件运行这个服务器一千次时,差异会变得更加明显:

Spawn 1000xtime/msspeedup
Node-API1.531s-
Request/Response-API5.29ms快 289 倍

1.5s5.2ms,几乎是瞬间完成,这使得测试工作变得更加愉快。

我如何启动我的服务器?

现在,公平地说,到目前为止我们还没有启动服务器。而这正是这个 API 的美妙之处,因为我们不需要启动!确切的 API 取决于正在使用的运行时环境,但通常它只是几行代码。例如,在 Deno 中,它看起来像这样:

Deno.serve(req => new Response("Hello world!"));

然而,比这更重要的是 WinterTC 组(前身为 WinterCG)已经标准化了直接导出你的应用函数。这意味着在不同的运行时环境中运行你的代码而无需更改你的代码(至少是服务器处理部分)要容易得多。

export default {
	fetch() {
		return new Response("hello world");
	},
};

结论

这个 API 今天在任何地方都有效!唯一的例外是 Node,尽管他们是 WinterTC 的一部分,但他们尚未发布此功能。但是通过一点点好的 polyfill,你可以教会它构建服务器的现代方法。一旦 Node 原生支持这一点,框架的大量工具将会变得更容易。

他们也倾向于尝试将每个运行时环境都变成 Node,这是一项庞大的任务,并且会引起很多摩擦。这就是我在为 Deno 的框架编写适配器和原型化新 API 时遇到的确切情况。

向 SvelteKit 致敬,它是为数不多的掌握了这方面的现代框架之一,并使编写适配器变得轻而易举!