什么叫做注水和脱水呢?这个和同构应用中数据的获取有关:在服务器端渲染时,首先服务端请求接口拿到数据,并处理准备好数据状态(如果使用 Redux,就是进行 store 的更新),为了减少客户端的请求,我们需要保留住这个状态。一般做法是在服务器端返回 HTML 字符串的时候,将数据 JSON.stringify 一并返回,这个过程,叫做脱水(dehydrate);在客户端,就不再需要进行数据的请求了,可以直接使用服务端下发下来的数据,这个过程叫注水(hydrate)。用代码来表示:
function getPromisesFromTree({
rootElement,
rootContext = {},
}: PromiseTreeArgument): PromiseTreeResult[] {
const promises: PromiseTreeResult[] = [];
walkTree(rootElement, rootContext, (_, instance, context, childContext) => {
if (instance && hasFetchDataFunction(instance)) {
const promise = instance.fetchData();
if (isPromise<Object>(promise)) {
promises.push({ promise, context: childContext || context, instance });
return false;
}
}
});
return promises;
}
// Recurse a React Element tree, running visitor on each element.
// If visitor returns `false`, don't call the element's render function
// or recurse into its child elements.
export function walkTree(
element: React.ReactNode,
context: Context,
visitor: (
element: React.ReactNode,
instance: React.Component<any> | null,
context: Context,
childContext?: Context,
) => boolean | void,
) {
if (Array.isArray(element)) {
element.forEach(item => walkTree(item, context, visitor));
return;
}
if (!element) {
return;
}
// A stateless functional component or a class
if (isReactElement(element)) {
if (typeof element.type === 'function') {
const Comp = element.type;
const props = Object.assign({}, Comp.defaultProps, getProps(element));
let childContext = context;
let child;
// Are we are a react class?
if (isComponentClass(Comp)) {
const instance = new Comp(props, context);
// In case the user doesn't pass these to super in the constructor.
// Note: `Component.props` are now readonly in `@types/react`, so
// we're using `defineProperty` as a workaround (for now).
Object.defineProperty(instance, 'props', {
value: instance.props || props,
});
instance.context = instance.context || context;
// Set the instance state to null (not undefined) if not set, to match React behaviour
instance.state = instance.state || null;
// Override setState to just change the state, not queue up an update
// (we can't do the default React thing as we aren't mounted
// "properly", however we don't need to re-render as we only support
// setState in componentWillMount, which happens *before* render).
instance.setState = newState => {
if (typeof newState === 'function') {
// React's TS type definitions don't contain context as a third parameter for
// setState's updater function.
// Remove this cast to `any` when that is fixed.
newState = (newState as any)(instance.state, instance.props, instance.context);
}
instance.state = Object.assign({}, instance.state, newState);
};
if (Comp.getDerivedStateFromProps) {
const result = Comp.getDerivedStateFromProps(instance.props, instance.state);
if (result !== null) {
instance.state = Object.assign({}, instance.state, result);
}
} else if (instance.UNSAFE_componentWillMount) {
instance.UNSAFE_componentWillMount();
} else if (instance.componentWillMount) {
instance.componentWillMount();
}
if (providesChildContext(instance)) {
childContext = Object.assign({}, context, instance.getChildContext());
}
if (visitor(element, instance, context, childContext) === false) {
return;
}
child = instance.render();
} else {
// Just a stateless functional
if (visitor(element, null, context) === false) {
return;
}
child = Comp(props, context);
}
if (child) {
if (Array.isArray(child)) {
child.forEach(item => walkTree(item, childContext, visitor));
} else {
walkTree(child, childContext, visitor);
}
}
} else if ((element.type as any)._context || (element.type as any).Consumer) {
// A React context provider or consumer
if (visitor(element, null, context) === false) {
return;
}
let child;
if ((element.type as any)._context) {
// A provider - sets the context value before rendering children
((element.type as any)._context as any)._currentValue = element.props.value;
child = element.props.children;
} else {
// A consumer
child = element.props.children((element.type as any)._currentValue);
}
if (child) {
if (Array.isArray(child)) {
child.forEach(item => walkTree(item, context, visitor));
} else {
walkTree(child, context, visitor);
}
}
} else {
// A basic string or dom element, just get children
if (visitor(element, null, context) === false) {
return;
}
if (element.props && element.props.children) {
React.Children.forEach(element.props.children, (child: any) => {
if (child) {
walkTree(child, context, visitor);
}
});
}
}
} else if (typeof element === 'string' || typeof element === 'number') {
// Just visit these, they are leaves so we don't keep traversing.
visitor(element, null, context);
}
}
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import Express from 'express'
import { StaticRouter } from 'react-router'
import { InMemoryCache } from "apollo-cache-inmemory"
import Layout from './routes/Layout'
// Note you don't have to use any particular http server, but
// we're using Express in this example
const app = new Express();
app.use((req, res) => {
const client = new ApolloClient({
ssrMode: true,
// Remember that this is the interface the SSR server will use to connect to the
// API server, so we need to ensure it isn't firewalled, etc
link: createHttpLink({
uri: 'http://localhost:3010',
credentials: 'same-origin',
headers: {
cookie: req.header('Cookie'),
},
}),
cache: new InMemoryCache(),
});
const context = {}
// The client-side App will instead use <BrowserRouter>
const App = (
<ApolloProvider client={client}>
<StaticRouter location={req.url} context={context}>
<Layout />
</StaticRouter>
</ApolloProvider>
);
// rendering code (see below)
})
这个做法也非常简单,原理是:服务端请求时需要保留客户端页面请求的信息,并在 API 请求时携带并透传这个信息。上述代码中,createHttpLink 方法调用时:
App 组件嵌入到 document.querySelector('#root') 节点当中,一般是不包含 head 标签的。但是单页应用在切换路由时,可能也会需要动态修改 head 标签信息,比如 title 内容。也就是说:在单页面应用切换页面,不会经过服务端渲染,但是我们仍然需要更改 document 的 title 内容。
那么服务端如何渲染 meta tags head 标签就是一个常被忽略但是至关重要的话题,我们往往使用 React-helmet 库来解决问题。