编写 JavaScript 服务器的现代方法
📖 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 最终会产生多少开销呢?
Benchmark | time/iter (avg) | iter/s |
---|---|---|
Node-API | 806.5 µs | 1,240 |
Request/Response-API | 3.0 µs | 329,300 |
嗯,事实证明相当多!这个基准测试可能使差异看起来很小,但是当假设我们有一个测试套件运行这个服务器一千次时,差异会变得更加明显:
Spawn 1000x | time/ms | speedup |
---|---|---|
Node-API | 1.531s | - |
Request/Response-API | 5.29ms | 快 289 倍 |
从 1.5s
到 5.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 致敬,它是为数不多的掌握了这方面的现代框架之一,并使编写适配器变得轻而易举!