浅谈React 服务器端渲染的使用
React提供了两个方法renderToString和renderToStaticMarkup用来将组件(VirtualDOM)输出成HTML字符串,这是React服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。
服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:
- 前后端可以共享代码
- 前后端路由可以统一处理
React生态提供了很多选择方案,这里我们选用Redux和react-router来做说明。
Redux
Redux提供了一套类似Flux的单向数据流,整个应用只维护一个Store,以及面向函数式的特性让它对服务器端渲染支持很友好。
2分钟了解Redux是如何运作的
关于Store:
- 整个应用只有一个唯一的Store
- Store对应的状态树(State),由调用一个reducer函数(rootreducer)生成
- 状态树上的每个字段都可以进一步由不同的reducer函数生成
- Store包含了几个方法比如dispatch,getState来处理数据流
- Store的状态树只能由dispatch(action)来触发更改
Redux的数据流:
- action是一个包含{type,payload}的对象
- reducer函数通过store.dispatch(action)触发
- reducer函数接受(state,action)两个参数,返回一个新的state
- reducer函数判断action.type然后处理对应的action.payload数据来更新状态树
所以对于整个应用来说,一个Store就对应一个UI快照,服务器端渲染就简化成了在服务器端初始化Store,将Store传入应用的根组件,针对根组件调用renderToString就将整个应用输出成包含了初始化数据的HTML。
react-router
react-router通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过props将路由信息传递给组件使用,所以只要路由变更,props就会变化,触发组件re-render。
假设有一个很简单的应用,只有两个页面,一个列表页/list和一个详情页/item/:id,点击列表上的条目进入详情页。
可以这样定义路由,./routes.js
importReactfrom'react'; import{Route}from'react-router'; import{List,Item}from'./components'; //无状态(stateless)组件,一个简单的容器,react-router会根据route //规则匹配到的组件作为`props.children`传入 constContainer=(props)=>{ return({props.children}); }; //route规则: //-`/list`显示`List`组件 //-`/item/:id`显示`Item`组件 constroutes=(); exportdefaultroutes;
从这里开始,我们通过这个非常简单的应用来解释实现服务器端渲染前后端涉及的一些细节问题。
Reducer
Store是由reducer产生的,所以reducer实际上反映了Store的状态树结构
./reducers/index.js
importlistReducerfrom'./list'; importitemReducerfrom'./item'; exportdefaultfunctionrootReducer(state={},action){ return{ list:listReducer(state.list,action), item:itemReducer(state.item,action) }; }
rootReducer的state参数就是整个Store的状态树,状态树下的每个字段对应也可以有自己的reducer,所以这里引入了listReducer和itemReducer,可以看到这两个reducer的state参数就只是整个状态树上对应的list和item字段。
具体到./reducers/list.js
constinitialState=[]; exportdefaultfunctionlistReducer(state=initialState,action){ switch(action.type){ case'FETCH_LIST_SUCCESS':return[...action.payload]; default:returnstate; } }
list就是一个包含items的简单数组,可能类似这种结构:[{id:0,name:'firstitem'},{id:1,name:'seconditem'}],从'FETCH_LIST_SUCCESS'的action.payload获得。
然后是./reducers/item.js,处理获取到的item数据
constinitialState={}; exportdefaultfunctionlistReducer(state=initialState,action){ switch(action.type){ case'FETCH_ITEM_SUCCESS':return[...action.payload]; default:returnstate; } }
Action
对应的应该要有两个action来获取list和item,触发reducer更改Store,这里我们定义fetchList和fetchItem两个action。
./actions/index.js
importfetchfrom'isomorphic-fetch'; exportfunctionfetchList(){ return(dispatch)=>{ returnfetch('/api/list') .then(res=>res.json()) .then(json=>dispatch({type:'FETCH_LIST_SUCCESS',payload:json})); } } exportfunctionfetchItem(id){ return(dispatch)=>{ if(!id)returnPromise.resolve(); returnfetch(`/api/item/${id}`) .then(res=>res.json()) .then(json=>dispatch({type:'FETCH_ITEM_SUCCESS',payload:json})); } }
isomorphic-fetch是一个前后端通用的Ajax实现,前后端要共享代码这点很重要。
另外因为涉及到异步请求,这里的action用到了thunk,也就是函数,redux通过thunk-middleware来处理这类action,把函数当作普通的actiondispatch就好了,比如dispatch(fetchList())
Store
我们用一个独立的./store.js,配置(比如ApplyMiddleware)生成Store
import{createStore}from'redux'; importrootReducerfrom'./reducers'; //Applymiddlewarehere //... exportdefaultfunctionconfigureStore(initialState){ conststore=createStore(rootReducer,initialState); returnstore; }
react-redux
接下来实现,
./app.js
importReactfrom'react'; import{render}from'react-dom'; import{Router}from'react-router'; importcreateBrowserHistoryfrom'history/lib/createBrowserHistory'; import{Provider}from'react-redux'; importroutesfrom'./routes'; importconfigureStorefrom'./store'; //`__INITIAL_STATE__`来自服务器端渲染,下一部分细说 constinitialState=window.__INITIAL_STATE__; conststore=configureStore(initialState); constRoot=(props)=>{ return(); } render({routes} ,document.getElementById('root'));
至此,客户端部分结束。
ServerRendering
接下来的服务器端就比较简单了,获取数据可以调用action,routes在服务器端的处理参考react-routerserverrendering,在服务器端用一个match方法将拿到的requesturl匹配到我们之前定义的routes,解析成和客户端一致的props对象传递给组件。
./server.js
importexpressfrom'express'; importReactfrom'react'; import{renderToString}from'react-dom/server'; import{RoutingContext,match}from'react-router'; import{Provider}from'react-redux'; importroutesfrom'./routes'; importconfigureStorefrom'./store'; constapp=express(); functionrenderFullPage(html,initialState){ return` ${html}