前言:在学习完
Jokcy
老师的这门课程后,打算搭建一个自己的服务端渲染模板,课程中状态管理选择的是mobx
,而我比较熟悉的是redux
,就选择了redux
搭配redux-saga
来进行构建,基于当前最新的react(16.4.1)、react-router(4.3.1)、redux(4.0.0)、redux-saga(0.16.0)、webpack(4.16.1)、express等构建react服务端渲染模板,后来在使用过程中觉得redux-saga
过于繁琐,已废弃!
git clone git@github.com:Kim09AI/react-ssr-template.git
cd react-ssr-template
yarn or npm install
# development
npm run dev:client
npm run dev:server
# 访问http://localhost:3000
# production
npm run build
npm start
# 访问http://localhost:3000
- react及redux的hot reload
- 区分环境
- 设置title及meta
- 路由懒加载(支持服务端渲染)
- eslint规范代码(airbnb)
- 内置css|stylus,支持css modules(默认启用)
- 使用pre-commit规范提交的代码
- 服务端渲染数据预取
- 设置title及meta
- 环境区分
- cssmodules
- 如何防止再次获取服务端渲染预取的数据
- 数据存在全局state中的问题
- 登录后刷新页面如何同步状态
- 路由懒加载的一些问题
- 存在的问题
- 目录结构
- 总结
在容器组件中添加一个bootstrap
的方法,在里面放置数据获取的代码,服务端渲染时会自动调用,可以在该方法内部返回一个promise
(可选),以下2种方式都可以
bootstrap() {
this.getPostDetailAsync()
}
bootstrap() {
return axios.get('/url')
}
既然页面初始化的数据可以通过组件的bootstrap
方法去获取数据,那回到浏览器这边也是可以的,所以对于页面初始数据的获取,不管在浏览器还是服务端,都不需要手动调用,只要定义好即可,在浏览器这边我写了一个高阶组件
去统一调用的,具体代码下面会看到
设置title及meta是通过react-helmet
来设置的,导入后在render
函数中设置
import Helmet from 'react-helmet'
<Helmet>
<title>{detail.title}</title>
<meta name="keywords" content="HTML,ASP,PHP,SQL" />
</Helmet>
通过判断process.env.isClient
或process.env.isServer
区分
基于react-css-modules
的css modules
,只支持css
和stylus
,只需import
,以及用styleName
替换className
,对于全局的样式,使用className
即可
import './style.styl'
<div styleName="test"></div>
对于服务端渲染预取的数据,回到浏览器时是不需要再次获取的,在容器组件添加一行即可@autoFetch
,不过添加的位置是有限制的,比如要添加@connect()
和@autoFetch
到Component
,那顺序只能是@connect() @autoFetch Component
,这个跟我的实现方式有关,而解决再次获取服务端渲染预取的数据的问题,主要是利用了组件的生命周期执行顺序
,代码如下
export const autoFetch = Component => {
class AutoFetchComponent extends Component {
componentWillMount() {
// eslint-disable-next-line
if (process.env.isClient && !window.__INITIAL_URL__) {
typeof super.bootstrap === 'function' && super.bootstrap()
}
typeof super.componentWillMount === 'function' && super.componentWillMount()
}
}
return AutoFetchComponent
}
// 根组件
export default class App extends React.Component {
componentDidMount() {
// 首屏渲染完成后删除 __INITIAL_URL__
// 接下来的页面初始数据获取就会自动获取
window.__INITIAL_URL__ && delete window.__INITIAL_URL__ // eslint-disable-line
}
}
浏览器的页面初始数据就是在componentWillMount
中统一获取的,window.__INITIAL_URL__
是服务端渲染时的url
(详细可查看server/ssr/render.js
),在页面刷新的时候,window.__INITIAL_URL__
是当前服务端渲染时的url
,然后componentWillMount
的时候发现存在window.__INITIAL_URL__
就不在获取初始数据了,然后componentDidMount
就删掉window.__INITIAL_URL__
,并且生命周期执行顺序如下
- 父组件componentWillMount
- 父组件render
- 子组件componentWillMount
- 子组件render
- 子组件componentDidMount
- 父组件componentDidMount
基于这样的生命周期执行顺序,在根组件
componentDidMount
时,所有组件的componentWillMount
都已执行,就可以利用window.__INITIAL_URL__
绕过数据获取了,componentDidMount
之后清除window.__INITIAL_URL__
,接下来的路由跳转就会自动获取数据了
在没有服务端渲染时,把数据存在store
的state
中,可能是因为我们的数据需要共享或需要缓存,而服务端渲染时同步预取数据需要借助redux
,导致一些适合放在组件state
的数据也都放到了全局state
,这样就会难免增加一些额外的判断,不加的话可能就会遇到这种情况,比如一个文章详情页,先点了一片文章然后返回,再点进另一篇文章时,就会先看到之前的文章,等ajax完成之后才看到当前文章,显然体验不够友好,针对这些问题,自己写了两个中间件cacheMiddleware
和resetStateMiddleware
,只需修改action
即可
export const getData = (forceUpdate) => ({
type: types.GET_POST_DETAIL_ASYNC,
reset: types.RESET_POST_DETAIL, // 放一个action type,作为真正获取数据的type前,先执行清理/reset
cache: state => state.detail.id === id // 指定是否需要缓存,接受全局state作为参数,
forceUpdate: true // 即使存在cache,也可以设置该项,忽略cache的结果,强制更新
})
在登录后刷新页面,显然是需要记住登录状态以及一些用户信息的,对于如何同步这些状态,可以考虑在src/app.jsx
中添加一个bootstrap
或在server/ssr/render.js
中同步,这里只是指出两个可能可以实现这一功能的地方
要使用路由懒加载的功能,同时还支持服务端渲染,react-loadable是一个不错的选择,基本按照文档step by step,就可以搭建好在production
模式下的路由懒加载,但是在development
模式下做服务端渲染时,却发现会出现找不到模块的错误,通过对比两个环境下的差异,很容易发现是因为一个打包在本地文件一个是打包在内存,而打包在内存中没法直接require
,那这样的话开发时也打包到本地不就行了,但是这样显然效率低下,于是就有了接下来的方法,再提供一份同步加载的组件,用于服务端渲染,这样所有js都打包到了一个文件,也就绕过了require
if (process.env.isServer && process.env.NODE_ENV === 'development') {
/* eslint-disable */
PostList = require('../containers/postList').default
/* eslint-enable */
} else {
PostList = Loadable({
loader: () => import(/* webpackChunkName: 'postList' */ '../containers/postList'),
loading: Loading
})
}
这样的话,对于需要懒加载的组件,就需要提供同时一个同步版本和一个异步版本,那这样就行了吗?刷新页面,发现服务端渲染正常,却发现报错说服务端渲染和客户端渲染结果不匹配,那是因为客户端渲染的时候,页面懒加载需要加载的js
还没加载好导致的,所以在客户端渲染前,需要先加载好该页面需要的js
,代码如下
;(async () => {
// 预加载当前页面匹配的页面组件
if (process.env.NODE_ENV === 'development') {
const components = getMatchComponents(routerConfig, window.location.pathname)
await Promise.all(components.map(component => component.preload && component.preload()))
}
Loadable.preloadReady().then(() => renderApp(App))
})()
cookie
的共享问题,有解决的方法但是使用起来不方便
学习完课程后,通过自己去从零搭建服务端渲染的工程,更加巩固的学到的知识,加深了对react服务端渲染的理解,搭建过程中也遇到了一些问题,在思考及解决某些问题的过程中也算有所提升,帮助自己在接下来可以搭建一个更加完善的服务端渲染模板