React Ssr
React SSR 总结
现在开发 React 基本都用 create-react-app
。
的确, opinionated
比 configurable
要方便,就像 prettier
比 linter
方便, create-react-app
比 webpack
方便。
当然,他们都不是一个层面的东西,不能这么比。
那么,怎么做 SSR 呢?
项目结构方面
// client.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { AProvider, BProvider } from "./ManyProviders";
// 1. 把 App等Component 导出出来,避免自己服务端代码还需要用各种 babel。。。。
export { App, AProvider, BProvider };
// 2. 把所有的在 import 阶段就会调用到 浏览器特有API 的代码用 isBrowser 包裹起来
const isBrowser =
typeof window !== "undefined" && typeof window.document !== "undefined";
if (isBrowser) {
const { default: registerServiceWorker } = require("./registerServiceWorker");
ReactDOM.render(
<AProvider>
<BProvider>
<App />
</BProvider>
</AProvider>,
document.getElementById("root")
);
registerServiceWorker();
}
// server.js
// 1.1 服务端不需要再考虑 babel等东西 了
const { AProvider, BProvider, App } = require("./build/static/js/main.xxxx.js");
// 3. 使用 Provider 的形式,让 bundle.js 内始终没有状态,状态都是从外面传进去的。
// 因为 server 是并发的,而 commonjs 的 module 没有并发
const app = (
<AProvider initialXXX={a_ctx}>
<BProvider initialXXX={b_ctx}>
<App />
</BProvider>
</AProvider>
);
server.js 里写的当然不是 jsx, 而是
React.createElement
, 这里只是偷个懒,下同。
renderToNodeStream
与 window.__INITIAL_STATE__
renderToNodeStream
得到的是一个 stream
, window.__INITIAL_STATE__ = "JSON.string(xxxx)"
是一个 string
, 他们都要被发送给浏览器。
而且,很多情况下, window.__INITIAL_STATE__
是要在 renderToNodeStream
结束后才能得到。
// server.js
const client = new ApolloClient(xxx);
const app = (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
await getDataFromTree(app);
// 接下来 renderToString 当然是很简单的,然后可以各种拼接字符串
// 但如果是 stream 呢? 下面仅仅是伪代码,只是为了提醒一件事情: stream 的拼接中,`window.__INITIAL_STATE__`的生成应该在 node_stream 结束之后
const node_stream = renderToNodeStream(app);
node_stream.pipe(res);
node_stream.on('end', _ => {
const __APOLLO_STATE__ = apollo_client.extract(true);
// const __APOLLO_STATE__JSON = JSON.stringify(__APOLLO_STATE__).replace(/</g, "\\u003c"); // 这里不是重点
res.write(`<script>window.__INITIAL_STATE__=${__APOLLO_STATE__JSON}</script>`);
res.write(`<script src="main.xxxxxx.js" />`)
});
stream
的拼接中,window.__INITIAL_STATE__
的生成应该在node_stream
结束之后。
bundle.js/main.js
应该在window.__INITIAL_STATE__
之后。。。或者bundle.js
里把hydrate/render
过程放在document.onload
中。
react-router
的 StaticRouter
与 renderToNodeStream
const router_ctx = {};
const app = (
<StaticRouter context={router_ctx}>
<App />
</StaticRouter>
);
// 我们可以全部渲染完之后,才知道是否应该 redirect
const html_str = renderToString(app);
if (router_ctx.url) {
return res.redirect(router_ctx.url);
}
// 但是如果用了 renderToNodeStream 之后要怎么办呢?
// http 可不支持传输 body 传到一半,突然来个 Redirect: 302 状态码
// https://github.com/ReactTraining/react-router/issues/6191
CSS in React
使用 @xialvjun/create-react-style.
直接在 node_stream
上渲染出 style
标签,避免后续的 stream
操作.
另外使用 hash
函数生成稳定的 className
, 或者抽取出 Provider
的 stylis_cache
.