现有这样一个需求,要提供一个接口,响应流式数据,其实就是日志信息,让客户端(调用方)能够实时展示服务器的日志。
如果是用Node.js的Express框架,可以这样轻松搞定:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.write("hello\n");
setTimeout(() => {
res.write("world");
res.end();
}, 5000);
});
app.listen(3000);
但在Deno中,我们使用的oak框架的官方文档中找不到响应流数据的例子,但看代码(deno.land/x/oak@v11.1…)是支持的。
export async function convertBodyToBodyInit(
body: ResponseBody | ResponseBodyFunction,
type?: string,
jsonBodyReplacer?: (key: string, value: unknown) => unknown,
): Promise<[globalThis.BodyInit | undefined, string | undefined]> {
let result: globalThis.BodyInit | undefined;
if (BODY_TYPES.includes(typeof body)) {
result = String(body);
type = type ?? (isHtml(result) ? "html" : "text/plain");
} else if (isReader(body)) {
result = readableStreamFromReader(body);
} else if (
ArrayBuffer.isView(body) || body instanceof ArrayBuffer ||
body instanceof Blob || body instanceof URLSearchParams
) {
// deno-lint-ignore no-explicit-any
result = body as any;
} else if (body instanceof ReadableStream) {
result = body.pipeThrough(new Uint8ArrayTransformStream());
} else if (body instanceof FormData) {
result = body;
type = "multipart/form-data";
} else if (isAsyncIterable(body)) {
result = readableStreamFromAsyncIterable(body);
} else if (body && typeof body === "object") {
result = JSON.stringify(body, jsonBodyReplacer);
type = type ?? "json";
} else if (typeof body === "function") {
const result = body.call(null);
return convertBodyToBodyInit(await result, type, jsonBodyReplacer);
} else if (body) {
throw new TypeError("Response body was set but could not be converted.");
}
return [result, type];
}
如果body传递了ReadableStream,则支持流式响应。
怎么构建ReadableStream呢?在Deno官方样例中有这样一个例子:
import { serve } from "https://deno.land/std@0.114.0/http/server.ts";
function handler(_req: Request): Response {
let timer: number | undefined = undefined;
const body = new ReadableStream({
start(controller) {
timer = setInterval(() => {
const message = `It is ${new Date().toISOString()}\n`;
controller.enqueue(new TextEncoder().encode(message));
}, 1000);
},
cancel() {
if (timer !== undefined) {
clearInterval(timer);
}
},
});
return new Response(body, {
headers: {
"content-type": "text/plain",
"x-content-type-options": "nosniff",
},
});
}
console.log("Listening on http://localhost:8000");
serve(handler);
看得出来,ReadableStream的start方法内置了controller参数,它的enqueue可以控制写入。再细看它的TypeScript定义:
/** @category Streams API */
interface ReadableStreamDefaultController<R = any> {
readonly desiredSize: number | null;
close(): void;
enqueue(chunk: R): void;
error(error?: any): void;
}
显而易见,close就是关闭或停止流的方法。
为方便起见,我们这样封装一段函数:
export interface ReadableStreamResult {
body: ReadableStream;
write(message: string): void;
end(message?: string): void;
}
export function getReadableStream(): ReadableStreamResult {
let controller: ReadableStreamDefaultController;
const body = new ReadableStream({
start(_controller) {
controller = _controller;
},
});
const te = new TextEncoder();
return {
body,
write(message: string) {
controller.enqueue(te.encode(message));
},
end(message?: string) {
if (message) {
controller.enqueue(te.encode(message));
}
controller.close();
},
};
}
使用:
const rs = getReadableStream();
response.body = rs.body;
rs.write("haha");
rs.end(globals.end_msg);
以下是个完整的样例:
import { Application } from "https://deno.land/x/oak@v11.1.0/mod.ts";
const app = new Application();
app.use((ctx) => {
const res = getReadableStream();
let { body, write, end } = res;
let timer: number | undefined = undefined;
let num = 0;
timer = setInterval(() => {
if (num === 5) {
clearInterval(timer);
return end();
}
num++;
const message = `It is ${new Date().toISOString()}\n`;
console.log(message);
write(message);
}, 1000);
ctx.response.body = body;
});
app.listen({ port: 3000 });
在控制台输入:curl http://localhost:3000
,效果如下:
这样就达到了流式效果,即客户端在一开始就能接收到响应,一直到服务器主动调用end结束。
但在具体业务中,发现并不是这么简单,因为我们的业务通常是异步的,把代码这样改下:
import { delay } from "https://deno.land/std@0.114.0/async/delay.ts";
app.use(async (ctx) => {
const res = getReadableStream();
const { body, write } = res;
ctx.response.body = body;
write("hello\n");
await test(res);
});
async function test(res: ReadableStreamResult) {
await delay(5000);
res.write("world\n");
res.end();
}
以上代码我们预期的是先响应hello,再过5秒响应world结束,但事实并非如此,而是过了5秒,hello world一起响应。如果你是用浏览器打开的http://localhost:3000,在网络里可以看到:
这是oak框架的中间件处理逻辑,因为test方法调用前加了await,这让框架以为你是要等待它结束后这个接口才算完成。
所以,要想达到预期的效果,只需要将这个await去掉即可。但这要求你的test方法必须做好容错处理,也就是说需要保障它不能失败,或者失败后处理好相应逻辑,主要是调用res.end()
来结束流。