入门 React DOM 服务端渲染
April 17, 2020
前言
当我们需要 React 服务端渲染时,需要了解 ReactDOMServer
这个对象。它能够使组件渲染成静态的 html 标记。
ReactDOMServer 提供了四个方法,其中 renderToString()
和 renderToStaticMarkup()
可以在浏览器和 node 环境中运行,renderToNodeStream()
和 renderToStaticNodeStream()
由于使用了 node 中特有的 stream,所以不能在浏览器中运行。
renderToString
执行原理
renderToString
作用
将 React 元素渲染为初始 HTML。React 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
hydrate()
作用
简单来讲这个方法是和 render() 的作用是相同的,只不过
hydrate()
在ReactDOMServer
渲染的容器中对 HTML 的内容进行补水操作。React 会尝试在已有标记上绑定事件监听器。
renderToString
的实现:
export function renderToString(element) {
const renderer = new ReactPartialRenderer(element, false);
try {
const markup = renderer.read(Infinity);
return markup;
} finally {
renderer.destroy();
}
}
该函数接收 react 组件参数并在内部实例化一个 ReactPartialRenderer
,该渲染器调用 read 方法返回 string 类型的标记。
class ReactDOMServerRenderer {
// ...
constructor(children: mixed, makeStaticMarkup: boolean) {
const flatChildren = flattenTopLevelChildren(children);
const topFrame: Frame = {
type: null,
// Assume all trees start in the HTML namespace (not totally true, but
// this is what we did historically)
domNamespace: Namespaces.html,
children: flatChildren,
childIndex: 0,
context: emptyObject,
footer: '',
};
if (__DEV__) {
((topFrame: any): FrameDev).debugElementStack = [];
}
this.threadID = allocThreadID();
this.stack = [topFrame];
this.exhausted = false;
this.currentSelectValue = null;
this.previousWasTextNode = false;
this.makeStaticMarkup = makeStaticMarkup;
this.suspenseDepth = 0;
// Context (new API)
this.contextIndex = -1;
this.contextStack = [];
this.contextValueStack = [];
if (__DEV__) {
this.contextProviderStack = [];
}
}
}
ReactPartialRenderer
的构造函数初始化了一些属性,其中 this.stack 初始值为只有顶级 frame 的数组。
renderer.read()
是内部的核心逻辑,该方法中 while 循环条件比较输出的 out 字符串长度,并进一步调用 render()
方法返回的字符串追加到 out 中。由于在 read 中传入的参数为 Infinity
,所以只有在 this.stack.length === 0
时才会 break 退出。
render()
方法内部根据了 child 节点类型是字符串、数字、react 组件分别执行对应的逻辑,如果 child 还有 child 节点,那么会被 push 到 this.stack 中,这样 read()
方法中的 while 循环就会继续解析该 child 的内容。
这其中还有非常重要的一个函数是 processChild(element, Component)
element 即为 child 节点,Component 为 element 上的 type 属性,即为我们传入的类组件或函数组件。
function processChild(element, Component) {
const isClass = shouldConstruct(Component);
const publicContext = processContext(Component, context, threadID, isClass);
let queue = [];
let replace = false;
const updater = {
isMounted: function(publicInstance) {
return false;
},
enqueueForceUpdate: function(publicInstance) {
if (queue === null) {
warnNoop(publicInstance, 'forceUpdate');
return null;
}
},
enqueueReplaceState: function(publicInstance, completeState) {
replace = true;
queue = [completeState];
},
enqueueSetState: function(publicInstance, currentPartialState) {
if (queue === null) {
warnNoop(publicInstance, 'setState');
return null;
}
queue.push(currentPartialState);
},
};
let inst;
if (isClass) {
inst = new Component(element.props, publicContext, updater);
if (typeof Component.getDerivedStateFromProps === 'function') {
const partialState = Component.getDerivedStateFromProps.call(
null,
element.props,
inst.state
);
if (partialState != null) {
inst.state = Object.assign({}, inst.state, partialState);
}
}
} else {
const componentIdentity = {};
prepareToUseHooks(componentIdentity);
inst = Component(element.props, publicContext, updater);
inst = finishHooks(Component, element.props, inst, publicContext);
// If the flag is on, everything is assumed to be a function component.
// Otherwise, we also do the unfortunate dynamic checks.
if (disableModulePatternComponents || inst == null || inst.render == null) {
child = inst;
validateRenderResult(child, Component);
return;
}
}
inst.props = element.props;
inst.context = publicContext;
inst.updater = updater;
let initialState = inst.state;
if (initialState === undefined) {
inst.state = initialState = null;
}
if (
typeof inst.UNSAFE_componentWillMount === 'function' ||
typeof inst.componentWillMount === 'function'
) {
if (typeof inst.componentWillMount === 'function') {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
if (typeof Component.getDerivedStateFromProps !== 'function') {
inst.componentWillMount();
}
}
if (
typeof inst.UNSAFE_componentWillMount === 'function' &&
typeof Component.getDerivedStateFromProps !== 'function'
) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
inst.UNSAFE_componentWillMount();
}
if (queue.length) {
const oldQueue = queue;
const oldReplace = replace;
queue = null;
replace = false;
if (oldReplace && oldQueue.length === 1) {
inst.state = oldQueue[0];
} else {
let nextState = oldReplace ? oldQueue[0] : inst.state;
let dontMutate = true;
for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
const partial = oldQueue[i];
const partialState =
typeof partial === 'function'
? partial.call(inst, nextState, element.props, publicContext)
: partial;
if (partialState != null) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
inst.state = nextState;
}
} else {
queue = null;
}
}
child = inst.render();
validateRenderResult(child, Component);
// 这里为了兼容历史的 context
let childContext;
if (disableLegacyContext) {
} else {
if (typeof inst.getChildContext === 'function') {
const childContextTypes = Component.childContextTypes;
if (typeof childContextTypes === 'object') {
childContext = inst.getChildContext();
for (const contextKey in childContext) {
invariant(
contextKey in childContextTypes,
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
getComponentName(Component) || 'Unknown',
contextKey
);
}
}
if (childContext) {
context = Object.assign({}, context, childContext);
}
}
}
return { child, context };
}
这里我们省略了 Dev 环境下的各种判断逻辑,其余部分首先判断了组件是类组件还是函数组件,然后对应执行 new Component()
或 Component()
来初始化组件实例,并对实例添加 props,state,updater 属性。在接下来根据组件是否声明了生命周期函数来进行相应的调用。最后调用实例上的 render()
方法赋值给 child 并返回。
总结
本文主要从 renderToString
入口来简单了解了一下 react 是如何根据传入的组件一步步渲染为标准的 html 字符串,可以直接在浏览器中渲染。那么给了我们一些启发,是不是我们可以有一个专门用来将 react 组件实时渲染为 html 的服务,然后将输出返回给调用方,这样我们甚至可以支持在线配置 react 组件并持久化下来支撑将来的业务需要。