好处

React16推出了流式服务端渲染,它允许你并行地分发HTML片段。这样可以让渲染
出的首字节有意义的内容给用户速度更快。

(例子1,上面部分是一次性转换,下面是流渲染,两种方式)
而且相对renderToString,流是异步的。这个可以让你的Node.js服务一次性渲染多个请求,并且保持在高请求压力环境下的及时响应。在一个持续的时间内,如果网络阻塞了,它可以停止React的渲染,并且不会因为重的网络请求影响其他轻请求。

实现: 用renderToNodeStream

import { renderToNodeStream } from 'react-dom/server';
import Frontend from '../client';

app.use('*', (request, response) => {
  // 一开始发送有意义的页面部分
  response.write('<html><head><title>Page</title></head><body><div id="root">');
  // 把组件渲染成流,并且给Response
  const stream = renderToNodeStream(<Frontend />);
  stream.pipe(response, { end: 'false' });
  // 当React渲染结束后,发送剩余的HTML部分给浏览器
  stream.on('end', () => {
    response.end('</div></body></html>');
  });
});

(以上是例子2)

流是怎么工作的

renderToNodeStream返回了一个可读流,可以比作水龙头:它有源源不断的水,只是被堵住了,等待别人把它拧开。如果用这个可读流(如果把水龙头打开),只需要监听“data”时间就可以了(如果有新数据到了会自动触发)
代码类似这样:

import { renderToNodeStream } from 'react-dom/server';
const stream = renderToNodeStream(<Frontend />);
stream.on('data', (data) => {
  console.log(JSON.stringify(data));
});

但是我们看例子2并没有监听”data“事件,而是直接把流pipe给了response对象。其实啊,
response对象在Node.js中是一个可写流,可以比做下水道。可读流通过pipe给可写流时,
可读流的数据会被分块发给可写流(就像是水龙头直接和下水道连接了一样,水源源不断流通)。
可写流可以对数据(水)进行任意的处理。

在我们的例子中,我们把组件渲染出来的流(html片段)给了response,response会把片段发给请求的客户端(比如浏览器)。


(可以看到First Byte明显前移动了,说明流渲染更快;因为并行的原因,片段渲染也更快了)

在Node.js中缓存HTML

const originalSend = response.send;
response.send = (html, ...other) => {
  // 保证不缓存错误状态
  if (response.statusCode > 100 && response.statusCode < 300) cache.put(request.path, html)
  originalSend(html, ...other);
}

采用Express的中间件,提供缓存文件服务:

app.use('*', (request, response, next) => {
  if (cache.has(request.path)) {
    //如果缓存中有这个路径,则返回HTML
    cache.get(request.path).then(html => response.send(html));
  } else {
    //否则就直接渲染
    next();
  }
})

接下来的问题是:如何渲染HTML,当你直接把htm的可写流pipe给了response流。因为response.send不会再被调用了,你也无法访问整个HTML文档。

缓存流HTML:transform streams

transform streams是什么?它就像是一个水龙头的过滤器。可读流经过它转换后才会给可写流。

在我们这里,我们只是想要缓存HTML并不会转化数据,具体怎么做,看代码:

import { Transform } from 'stream';

const createCacheStream = (key) => {
  const bufferedChunks = [];
  return new Transform({
    //每一个HTML片段都会调用 transform() 
    transform(data, enc, cb) {
      // 把HTML片段(Buffer)保存在内存中
      bufferedChunks.push(data);
      // 数据原封不动地给到下一个可读流
      cb(null, data);
    },

    // 当所有的事件都做完后会调用flush()
    flush(cb) {
      // 通过连接HTML片段得到完整的html
      // 并缓存起来
      cache.set(key, Buffer.concat(bufferedChunks))
      cb();
    }
  });
}

然后我们可以在我们的服务端渲染代码中使用它:

app.use('*', (request, response) => {
  // 通过过滤器(transform streams)创建了缓存
  let cacheStream = createCacheStream(request.path);
  // cachesStream接上Response
  cacheStream.pipe(response);
  //先把首部分写到cacheStream
  cacheStream.write('<html><head><title>Page</title></head><body><div id="root">');
  // 创建了渲染流
  const renderStream = renderToNodeStream(<Frontend />);
  // 渲染流接上cacheStream
  renderStream.pipe(cacheStream, { end: false });
  renderStream.on('end', () => {
    // 一旦结束渲染则做剩余部分的输出
    cacheStream.end('</div></body></html>');
  });
})

以上就是主要内容

原文链接:
https://zeit.co/blog/streaming-server-rendering-at-spectrum?nsukey=uW6M5M2uTd6F3Nc1u6tAVuAMGhbsBnpEuVplgUQLBlKevYZGGYN%2Bi1m6RocmDu%2F9367Zl71zSNn%2BkcWnaXcdX7bmeFLEyjbMMVsl5VtaBTfAvX8iNXgXMdbYWxVuuAealdKz8wkNfwEQtp4yLxknnredPgKISZUp%2BuR420YfPbpg8dW1lJMnpBMwNAJYMFZ0
作者知乎/公众号:前端疯 (一群热爱前端的一线程序员维护,想要用前端改变世界。)

posted on 2018-03-31 19:16  yellman  阅读(1397)  评论(0编辑  收藏  举报