由于Ivan 链接的question and answer 中提到的问题,我认为您无法在此处完成“最正确”的解决方案。至少我的 Chrome 和 Firefox 可以毫不费力地逐行渲染他们收到的最新内容,但是,正如上面所说,它需要修改或更改要求以使其更加透明。
这里要做的第一件事是获取但抑制前导 n 个字节以触发浏览器呈现。
如果您使用text/plain,则只能依赖特定浏览器呈现输出文本的方式。为了抑制第一个虚拟块输出,您可以只渲染空格,因为它们不打算由人类或浏览器解析(至少我认为是这样,因为您想要在浏览器中输出,因此可能不会使其成为机器 -可解析)。这里的一个技巧是编写 Unicode \u200B (zero width space),希望目标浏览器会使用它,在输出窗口中不渲染任何内容。不幸的是,我的 Firefox 实例无法识别该字符并呈现默认的未知字符占位符。然而,Chrome 完全忽略了这些字符,在视觉上它们看起来什么都没有!这似乎是你所需要的。所以,这里的一般算法是:
但是,如果您不希望出现像 Firefox 那样的 \u200B 字符的输出渲染问题,您可能需要切换到 text/html。 HTML 支持标记 cmets,因此我们可以将某些内容排除在渲染之外。这允许完全依赖 HTML,而不是特定的浏览器细节。知道了这一点,算法就变得有些不同了:
- 检测用户代理以确定标头块长度。
- 使用
<!-- 渲染块的开头,然后是一些n 个空格(但据我所知至少有一个;或者任何HTML 注释),然后是-->。 n 应该是上面块的长度减去注释开始/结束标记的长度。
- 生成一些虚拟输出,其中每一行都经过 HTML 转义,以
<br/> 或 <br> 终止,然后立即刷新。
对我来说,这种方法在 Chrome 和 Firefox 中都可以正常工作。如果你对一些 Java 没问题,这里有一些实现上述内容的代码:
@RestController
@RequestMapping("/messages")
public final class MessagesController {
private static final List<String> lines = asList(
"Lorem ipsum dolor sit amet,",
"consectetur adipiscing elit,",
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
);
@RequestMapping(value = "html", method = GET, produces = "text/html")
public void getHtml(final HttpServletRequest request, final ServletResponse response)
throws IOException, InterruptedException {
render(Renderers.HTML, request, response);
}
@RequestMapping(value = "text", method = GET, produces = "text/plain")
public void getText(final HttpServletRequest request, final ServletResponse response)
throws IOException, InterruptedException {
render(Renderers.PLAIN, request, response);
}
private static void render(final IRenderer renderer, final HttpServletRequest request, final ServletResponse response)
throws IOException, InterruptedException {
final int stubLength = getStubLength(request);
final ServletOutputStream outputStream = response.getOutputStream();
renderer.renderStub(stubLength, outputStream);
renderInfiniteContent(renderer, outputStream);
}
private static int getStubLength(final HttpServletRequest request) {
final String userAgent = request.getHeader("User-Agent");
if ( userAgent == null ) {
return 0;
}
if ( userAgent.contains("Chrome") ) {
return 1024;
}
if ( userAgent.contains("Firefox") ) {
return 1024;
}
return 0;
}
private static void renderInfiniteContent(final IRenderer renderer, final ServletOutputStream outputStream)
throws IOException, InterruptedException {
for ( ; ; ) {
for ( final String line : lines ) {
renderer.renderLine(line, outputStream);
sleep(5000);
}
}
}
private interface IRenderer {
void renderStub(int length, ServletOutputStream outputStream)
throws IOException;
void renderLine(String line, ServletOutputStream outputStream)
throws IOException;
}
private enum Renderers
implements IRenderer {
HTML {
private static final String HTML_PREFIX = "<!-- ";
private static final String HTML_SUFFIX = " -->";
private final int HTML_PREFIX_SUFFIX_LENGTH = HTML_PREFIX.length() + HTML_SUFFIX.length();
@Override
public void renderStub(final int length, final ServletOutputStream outputStream)
throws IOException {
outputStream.print(HTML_PREFIX);
for ( int i = 0; i < length - HTML_PREFIX_SUFFIX_LENGTH; i++ ) {
outputStream.write('\u0020');
}
outputStream.print(HTML_SUFFIX);
outputStream.flush();
}
@Override
public void renderLine(final String line, final ServletOutputStream outputStream)
throws IOException {
outputStream.print(htmlEscape(line, "UTF-8"));
outputStream.print("<br/>");
}
},
PLAIN {
private static final char ZERO_WIDTH_CHAR = '\u200B';
private final byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
@Override
public void renderStub(final int length, final ServletOutputStream outputStream)
throws IOException {
outputStream.write(bom);
for ( int i = 0; i < length; i++ ) {
outputStream.write(ZERO_WIDTH_CHAR);
}
outputStream.flush();
}
@Override
public void renderLine(final String line, final ServletOutputStream outputStream)
throws IOException {
outputStream.println(line);
outputStream.flush();
}
}
}
}
此外,您想要完成的方法不会向下滚动浏览器窗口。您可能希望在 Chrome 中使用用户脚本来自动向下滚动特定的 URL 页面,但据我所知,它不适用于 text/plain 输出。