//...koa app.js //build出来的ssr.js const SSR = require('./build/node/ssr'); //preload all components on server side, 服务端没有动态加载各个组件,提前先加载好 SSR.preloadAll(); //实例化一个SSR对象 const s = new SSR(); router.get('*', async (ctx) => { //根据路由,渲染不同的页面组件 const rendered = s.render(ctx.url); const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <div>${rendered.html}</div> <script type="text/javascript" src="https://www.jb51.net/runtime.js"></script> ${rendered.scripts.join()} <script type="text/javascript" src="https://www.jb51.net/app.js"></script> </body> </html> `; ctx.body = html; }); //...
以上是个简单的实现React SSR到koa web server, 为了使react-loadable知道哪些组件在服务端渲染了,rendered里面的scripts数组里面包含了SSR过的组件组成的各个script标签,里面调用了SSR#generateBundleScripts()方法,在插入时需要确保这些script标签在runtime.js之后((通过 CommonsChunkPlugin 来抽出来)),并且在app bundle之前(也就是初始化的时候应该已经知道之前的哪些组件已经渲染过了)。更多react-loadable服务端支持,参考这里.
上面我们还把react-router的路由都单独抽出去了,使得它可以运行在浏览器跟服务端,以下是AppRoutes组件:
//AppRoutes.js import Loadable from 'react-loadable'; //... const AsyncHello = Loadable({ loading: <div>loading...</div>, loader: () => import('./Hello'), }) function AppRoutes(props) { <Switch> <Route exact path="/hello" component={AsyncHello} /> <Route path="https://www.jb51.net/" component={Home} /> </Switch> } export default AppRoutes //然后在 App.js 入口中 import AppRoutes from './AppRoutes'; // ... export default () => { return ( <Router> <AppRoutes/> </Router> ) }
服务端渲染的初始状态
目前为止,我们已经创建了一个React SPA,并且能在浏览器端跟服务端共同运行🍺,社区称之为universal app 或者 isomophic app。但是我们现在的app还有一个遗留问题,一般来说我们app的数据或者状态都需要通过远端的api来异步获取,拿到数据后我们才能开始渲染组件,服务端SSR也是一样,我们要动态的获取初始数据,然后才能扔给React去做SSR,然后在浏览器端我们还要初始化就能同步获取这些SSR时的初始化数据,避免浏览器端初始化时又重新获取了一遍。
下面我们简单从github获取一些项目的信息作为页面初始化的数据, 在koa的app.js中:
//... const fetch = require('isomorphic-fetch'); router.get('*', async (ctx) => { //fetch branch info from github const api = 'https://api.github.com/repos/jasonboy/wechat-jssdk/branches'; const data = await fetch(api).then(res => res.json()); //传入初始化数据 const rendered = s.render(ctx.url, data); const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <div>${rendered.html}</div> <script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script> <script type="text/javascript" src="https://www.jb51.net/runtime.js"></script> ${rendered.scripts.join()} <script type="text/javascript" src="https://www.jb51.net/app.js"></script> </body> </html> `; ctx.body = html; });
然后在你的Hello组件中,你需要checkwindow里面(或者在App入口中统一判断,然后通过props传到子组件中)是否存在window.__INITIAL_DATA__,有的话直接用来当做初始数据,没有的话我们在componentDidMount生命周期函数中再去来数据:
export default class Hello extends React.Component { constructor(props) { super(props); this.state = { //这里直接判断window,如果是父组件传入的话,通过props判断 github: window.__INITIAL_DATA__ || [], }; } componentDidMount() { //判断没有数据的话,再去请求数据 //请求数据的方法也可以抽出去,以让浏览器及服务端能统一调用,避免重复写 if (this.state.github.length <= 0) { fetch('https://api.github.com/repos/jasonboy/wechat-jssdk/branches') .then(res => res.json()) .then(data => { this.setState({ github: data }); }); } } render() { return ( <div> <ul> {this.state.github.map(b => { return <li key={b.name}>{b.name}</li>; })} </ul> </div> ); } }