钟钦成
钟钦成,网名为司徒正美,国内著名的前端专家,对浏览器兼容性问题/选择器引擎/react 内部机制等具有深厚的积累,开发有 avalon/anu 等前端框架,著有《JavaScript框架设计》一书。
React16重写了 SSR ,它同时支持4种渲染方案,两种字符串渲染,两种流式渲染。
这四种方案的底层都是基于 ReactPartialRenderer 。流式渲染方案就是中间再搞一个
ReactMarkupReadableStream ,实现按块读取。如果没有流 stream ,都是一口气将所有内容拼成一个字符串返回前端,但这样做,如果内容太多,响应就很慢。如果每次返回一部分,浏览器就会一点点显示出来,体验会好很多。这个按块读取技术,就需要我们继承 Stream 。
我们需要把精力集中在 ReactPartialRenderer ,那4个渲染方法就随便过眼一下吧。
好了,进入正题, 我们着手将虚拟 DOM 转换为字符串。本文通篇都是讲如何转换字符串的,而转换字符串首先要获得原生虚拟 DOM 。
原生虚拟 DOM 又是什么呢?虚拟 DOM 为两大类,原生虚拟 DOM 与组件虚拟 DOM 。原生虚拟 DOM 对应浏览器中的元素节点。我们又留意到 JSX 里面可以使用单个字符串,数字放进 children 中。这些简单类型最后原封不动,不会变成虚拟 DOM ,但它们实质上就是浏览器中的文本节点。因此文本节点没什么所谓的转换。
而组件虚拟 DOM ,全名叫合成组件虚拟 DOM ,是其他组件虚拟 DOM 与原生虚拟 DOM 合并而成。它们最后都会归化为原生虚拟 DOM 。
原生虚拟 DOM 的转换
ReactPartialRenderer 内部有一个叫 ReactDOMServerRenderer 的类,它有一个叫 renderDOM 的方法,专门用于序列化原生虚拟 DOM 。 ReactDOMServerRenderer 是一个单例,它有一个stack数组,用来放一种叫 Frame 的对象。 Frame 是由虚拟 DOM 转换过来的。
Frame 的结构如下:
Frame 拥有这些属性, type 为标签名,如果它是 null 则不会产生内容, domNamespace 是命名空间,因此 SVG 与 HTML 的属性转换是有区别的。 children 是一个数组,装着待处理的子组件, childIndex 是它在父节点的索引值,如果为最后一个,则需要调用 footer 。 context 是上下文对象。 footer 是闭标签,如果是 br 这些空标签,那么没有闭标签,直接给“”。
于是问题来了,开标签哪里去了? renderDOM 直接返回了开标签。
开标签包含了 type, attributes 。 attributes 的转换规则有点复杂,首先它会过滤掉 key,ref,children,dangerousSetInnerHTML,innerHTML 等内置属性或涉及到 innerHTML 的属性。然后过滤掉里面是函数的属性,这些属性可能是 render props ,也可能是 onXXX 事件回调,再过滤掉一些布尔属性,为 false 时它们相当于空字符串,也不需要输出来。总之它们会转换成一个长长的字符串加到 openTag 的豪华大餐中。
renderDOM 的逻辑就很明确了,生成一个 Frame 放进 stack 中,返回开标签。
组件虚拟 DOM 的转换
renderDOM 包含在一个叫 render 的方法,它的行为也一样,产生一个 Frame 对象,返回一个 html 字符串或空字符串。
resolve 是最复杂的,它接受一个虚拟 DOM 与 context ,然后内部生成一个组件实例与新的 context ,经过两个钩子,然后 render 得到新的孩子, 最后返回新孩子与新 context 。
如何驱动起来
我们看一下构造函数 new ReactPartialRenderer(element, boolean) ,第一个就是虚拟 DOM ,它会转换成第一个 Frame 对象放到 stack 中。有了第一个 Frame ,我们就可以在 read 方法得到其他 Frame 。 read 的逻辑如下,它有一个参数,决定输出字符串的长度。最初时字符串为零,然后我们将它 pop 出来(因为这是 stack ,先进后出),进行 render ,就能得到开标签与下一个 Frame ,开标签加到输出字符串中,看有没有超出长度,没有则继续展开下一个 Frame,Frame 中一个 childIndex 属性,用来判定这个 Frame 是某个父元素的最后一个孩子,那么这时需要加上 footer (闭标签)。如此下去,只要没有超出长度,就把整棵虚拟 DOM 树跑完。有人会问,字符串模式与流模式都有静态模式(Static),这有什么区别呢?这是在注入元素属性时然后加上一个 data-reactid 的东西,那么前端就可以跟根这 ID 进行一一匹配,不需要再次生成这些节点。
总结
React16的 SSR 基于性能考虑没有复用它的 Reconciler 代码,能一下子实现4种特定模式下的渲染方法,真可谓是大道至简。当然它也没有考虑许多细枝末叶找茬 东西,比如说万一组件在 constructor,componentWillMount,getDeviedStateFromProps,render 中出错怎么办, 要不要调 componentDidCatch 补救呢? React16的 SSR 没有考虑这些问题。
后记
React16的 SSR 只是渲染纯页面,对许多环境没有进行优化,比如说 SPA 下的动态加载页面, CSS 如何减少请求注入到页面首部,一个前后共用的数据管理库与路由器,一个稳定的 nodejs 渲染服务。这一切,都需要另一个框架来做。或许 React 将自己定义为一个纯视图库限制了它的发挥,或许它不想抢社区人士的功劳,让第三方发光发亮。总而言之,在实际业务中,光是官方的那个 SSR 是不够的,但很多时候,第三方库连它这个 SSR 功能都没有用到,自己实现了。 React 生态圈就是强大到如此地步,不断反哺 core 库。如果大家真想用 SSR ,不防试试 next.js。over 。
领取专属 10元无门槛券
私享最新 技术干货