Skip to content

Terence-Cheng/react-redux-ssr-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react redux 服务端渲染(同构)实战总结

标签(空格分隔): react redux 服务端渲染 同构


说明

此项目大部分参考了Jokcy在某网课的教程,然后加以实践与改进

有关服务端渲染

关于什么是服务端渲染,为什么用服务端渲染及什么是同构这里就不多做介绍,关于它们的介绍很多,可参考链接

渲染流程

st=>start: 在浏览器中输入url
op=>operation: 步骤一:node中接受到此请求
op2=>operation: 步骤二:执行server-entry.js输出的函数createApp,得到react组件
op3=>operation: 步骤三:执行bootstrap函数,一般是异步请求,用来获取页面的数据,返回一个Promise对象
op4=>operation: 步骤四:执行renderToString方法拿到页面html的字符串
op5=>operation: 步骤五:执行store.getState()获取页面的初始化store,页面的title meta等标签
op6=>operation: 步骤六:将上面两个步骤获得的数据,传入到到ejs模板中,并res.send来返回给浏览器
op7=>operation: 步骤七:执行客户端入口app.js,获取上述步骤设置的初始化store数据并挂载react组件
e=>end

st->op->op2->op3->op4->op5->op6->op7->e

渲染流程

关键点

  • 客户端与服务端用到的组件是一样的,但是两者的入口不一样。 服务端组件的入口server-entry.js代码
import React from 'react'
import { StaticRouter } from 'react-router-dom'
import { HelmetProvider } from 'react-helmet-async' // 设置页面的tdk等seo信息
import { Provider } from 'react-redux'
import configureStore from './store/store'
import App from './views/App' // 与客户端公用的react组件入口

const store = configureStore()

// 导出store对象,目的是在服务端请求完数据后通过store.getState()获取store的初始值,以便供客户端使用
export { store }

/**
 * 服务端组件渲染的入口——在服务端执行
 * @param routerContext,传入一个空对象,可在node中判断是否有重定向
 * @param url 当前页面请求的url
 * @param helmetContext 传入一个空对象,可在node中获取title description 等信息
 * @returns {*}
 */
export default (routerContext, url, helmetContext) => (
  <Provider store={store}>
    <StaticRouter context={routerContext} location={url}>
      <HelmetProvider context={helmetContext}>
        <App />
      </HelmetProvider>
    </StaticRouter>
  </Provider>
)
  • react-async-bootstrapper 具体介绍可看链接 可在react组件上执行方法,可用于数据的获取。 可在服务端执行bootstrap方法用来获取页面的数据,注意返回一个promise
import Helmet from 'react-helmet-async'
export default class Index extends React.Component {
  bootstrap() { // react组件定义boostrap方法,在服务端执行
    const { getTopicList } = this.props
    return getTopicList()
  }
  render() {
    const { topicList } = this.props
    return (
      <div>
        <Helmet> {/* 可添加页面的title meta link 等标签*/}
          <title>cnode社区标题</title>
        </Helmet>
        <div>{JSON.stringify(topicList)}</div>
      </div>
    )
  }
}
  • react-helmet-async 具体介绍可见链接 可在react组件中设置title meta等标签

  • 在浏览器中输入url,页面的请求被node接受到后。 获取react 组件 执行bootstrap方法,加载页面初始数据 通过renderToString方法拿到react组件的html字符串 拿到初始store 页面title meat等标签 res.send()返回给浏览器页面

    const bootstrapper = require('react-async-bootstrapper')
    const store = bundle.store // 获取server-entry导出的store
    const createApp = bundle.default // 获取server-entry导出的函数

    const routerContext = {} // 传给createApp方法,用来获取页面的url等信息,判断是否有重定向
    const helmetContext = {} // 传给createApp方法,用来获取title meta等信息

    const app = createApp(routerContext, req.url, helmetContext) /* 步骤二 */

    bootstrapper(app, {}, { /******步骤三:重点 用来执行react组件中设置的bootstrap方法,来获取页面数据******/
      reqHeaders: req.headers // 设置react context的内容,后面会介绍这么设置的目的
    }).then(() => {
      if (routerContext.url) {
        res.redirect(routerContext.url)
        res.end()
        return
      }
      const content = ReactDomServer.renderToString(app) /* 步骤四 */
      const { helmet } = helmetContext // 一定要写在renderToString后面,否则取不到改变的值

      const html = ejs.render(template, {
        appString: content,
        initialState: JSON.stringify(store.getState()), // 步骤五:把页面的store数据传入到html模板中,在html中通过设置一个window的全局变量来获取此值
        meta: helmet.meta.toString(), // 步骤五:获取react组件中设置的meta标签
        title: helmet.title.toString(),
        style: helmet.style.toString(),
        link: helmet.link.toString(),
        globalInfo: JSON.stringify({
          path: req.path // 添加path属性,判断客户端是否二次渲染时使用,后面会具体介绍
        })
      })
      res.send(html) // 步骤六 返回给浏览器页面
      resolve()
    }).catch(reject)
  • html模板设置
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  
  <%%- meta %>

  <%%- title %>

  <%%- link %>

  <%%- style %>

</head>
<body>
<div id="root"><%%- appString %></div>

<script>
  window.__INITIAL__STATE__ = <%%- initialState%>; /* 步骤六  设置页面的初始state */
  window.__GLOBAL__INFO__ = <%%- globalInfo%>;
</script>
</body>
</html>
  • 页面呈现,加载js后就会执行客户端的入口,关键点就是获取服务端传入的初始化store数据
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { AppContainer } from 'react-hot-loader' 
import { HelmetProvider } from 'react-helmet-async'

import App from './views/App'
import configureStore from './store/store'

const initialState = window.__INITIAL__STATE__ || {} // 步骤七:获取页面的初始store,达到服务端与客户端数据的统一

const store = configureStore(initialState)

const root = document.getElementById('root')
const render = (Component) => {
  ReactDOM.hydrate(
    <AppContainer>
      <Provider store={store}>
        <BrowserRouter>
          <HelmetProvider>
            <Component />
          </HelmetProvider>
        </BrowserRouter>
      </Provider>
    </AppContainer>,
  root,
)
}

遇到的其他问题

  • 服务端执行bootstrap发起请求获取数据时,请求头等信息会丢失,比如req.headers,ip等信息。 解决方案:bootstrap方法的第三个参数中的数据会设置到react context里。具体说明见文档
// \server\util\server-render.js
 bootstrapper(app, {}, {
  reqHeaders: req.headers // 设置react context的内容,把请求头传入
})
//把获取数据的方法添加到react组件原型中,React.Component.prototype.$axiosGet = get,这样调用方法时可判断context中是否有此信息,进而设置请求头
  • 服务端渲染已经请求过页面的初始数据,客户端加载页面后无需再次发起相同的请求。 但此页面如果没有进行服务端渲染,而是直接客户端渲染,这时需要在客户端加载数据。因此需要封装一个方法,获取页面的初始数据在服务端或者客户端执行仅且执行一次。
// \server\util\server-render.js
const html = ejs.render(template, {
        appString: content,
        initialState: JSON.stringify(store.getState()), 
        meta: helmet.meta.toString(),
        title: helmet.title.toString(),
        style: helmet.style.toString(),
        link: helmet.link.toString(),
        globalInfo: JSON.stringify({
          path: req.path // 添加path属性,判断客户端是否二次渲染时使用,在客户端发起请求时判断pathname是否与此path相等,如果相等则不请求
        })
      })
      res.send(html)
// \client\global\load-init-data.js
export default function loadInitData(pathname, callback) {
  const globalInfo = window.__GLOBAL__INFO__ 

  if (globalInfo.path === pathname) {
    // 服务端渲染的页面,无需再次请求相同的数据
    globalInfo.path = false
  } else if (typeof callback === 'function') {
    // 客户端渲染执行回调并返回
    return callback()
  }
  return new Promise(resolve => resolve(true))
}

客户端渲染页面出现的情景:浏览器中显示服务端渲染的页面后,点击链接后尽管url改变,但是本质上是调用的history api,即无刷新更改页面地址栏,这时不会进行上述的服务端渲染。

  • 客户端发起异步请求时直接写相对url即可,但是服务端发起请求必须是绝对url,因此定义一个node环境变量即可。
// \build\webpack.base.server.js webpack 配置
plugins: [
    new webpack.DefinePlugin({
      'process.env.API_BASE': JSON.stringify('http://127.0.0.1:' + serverPort)
    })
]
const baseUrl = process.env.API_BASE || '' // 服务端渲染请求的url必须是绝对路径
  • import css 使用style-loader失效,因为服务端没有document对象,因此使用提取css文件的形式 后面在总结项目时,发现webpack-isomorphic可以设置style里面的样式

About

react redux 服务端渲染(同构)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published