详解React服务端渲染从入门到精通(2)

这是什么意思呢?也就是说首屏渲染的网页一般要去请求外部数据,我们希望在生成HTML之前,去获取到这个页面需要的所有数据,然后塞到页面中去,这个过程,叫做“脱水”(Dehydrate),生成HTML返回给浏览器。浏览器拿到带着数据的HTML,去请求浏览器端js,接管页面,用这个数据来初始化组件。这个过程叫“注水”(Hydrate)。完成服务端与浏览器端数据的统一。

为什么要这么做呢?试想一下,假设没有数据的预获取,直接返回一个没有数据,只有固定内容的HTML结构,会有什么结果呢?

第一:由于页面内没有有效信息,不利于SEO。

第二:由于返回的页面没有内容,但浏览器端JS接管页面后回去请求数据、渲染数据,页面会闪一下,用户体验不好。

我们使用Redux来管理状态,因为有服务端代码和浏览器端代码,那么就分别需要两个store来管理服务端和浏览器端的数据。

组件的配置

组件要在服务端渲染的时候去请求数据,可以在组件上挂载一个专门发异步请求的方法,这里叫做loadData,接收服务端的store作为参数,然后store.dispatch去扩充服务端的store。

class Home extends React.Component { componentDidMount() { this.props.callApi() } render() { return <div>{this.props.state.name}</div> } } Home.loadData = store => { return store.dispatch(callApi()) } const mapState = state => state const mapDispatch = {callApi} export default connect(mapState, mapDispatch)(Home)

路由的改造

因为服务端要根据路由判断当前渲染哪个组件,可以在这个时候发送异步请求。所以路由也需要配置一下来支持loadData方法。服务端渲染的时候,路由的渲染可以使用react-router-config这个库,用法如下(重点关注在路由上挂载loadData方法):

import { BrowserRouter } from 'react-router-dom' import { renderRoutes } from 'react-router-config' import Home from './Home' export const routes = [ { path: 'https://www.jb51.net/', component: Home, loadData: Home.loadData, exact: true, } ] const Routers = <BrowserRouter> {renderRoutes(routes)} <BrowserRouter/>

服务端获取数据

到了服务端,需要判断匹配的路由内的所有组件各自都有没有loadData方法,有就去调用,传入服务端的store,去扩充服务端的store。同时还要注意到,一个页面可能是由多个组件组成的,会发各自的请求,也就意味着我们要等所有的请求都发完,再去返回HTML。

import express from 'express' import serverRender from './render' import { matchRoutes } from 'react-router-config' import { routes } from '../routes' import serverStore from "../store/serverStore" const app = express() app.get('*', (req, res) => { const context = {css: []} const store = serverStore() // 用matchRoutes方法获取匹配到的路由对应的组件数组 const matchedRoutes = matchRoutes(routes, req.path) const promises = [] for (const item of matchedRoutes) { if (item.route.loadData) { const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) }) promises.push(promise) } } // 所有请求响应完毕,将被HTML内容发送给浏览器 Promise.all(promises).then(() => { // 将生成html内容的逻辑封装成了一个函数,接收req, store, context res.send(serverRender(req, store, context)) }) })

细心的同学可能注意到了上边我把每个loadData都包了一个promise。

const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) console.log(item.route.loadData(store)); }) promises.push(promise)

这是为了容错,一旦有一个请求出错,那么下边Promise.all方法则不会执行,所以包一层promise的目的是即使请求出错,也会resolve,不会影响到Promise.all方法,也就是说只有请求出错的组件会没数据,而其他组件不会受影响。

注入数据

我们请求已经发出去了,并且在组件的loadData方法中也扩充了服务端的store,那么可以从服务端的数据取出来注入到要返回给浏览器的HTML中了。

来看 serverRender 方法

const serverRender = (req, store, context) => { // 读取客户端生成的HTML const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString( <Provider store={store}> <StaticRouter location={req.path} context={context}> <Container/> </StaticRouter> </Provider> ) // 注入数据 const initialState = `<script> window.context = { INITIAL_STATE: ${JSON.stringify(store.getState())} } </script>` return template.replace('<!--app-->', content) .replace('<!--initial-state-->', initialState) }

浏览器端用服务端获取到的数据初始化store

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/f3e6aa452a84db9f182252021d16a6e8.html