标签(空格分隔): 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里面的样式