React 提供了两个方法 renderToString 和 renderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。
服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:
- 前后端可以共享代码
- 前后端路由可以统一处理
React 生态提供了很多选择方案,这里我们选用 Redux 和 react-router 来做说明。
Redux
Redux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。
2 分钟了解 Redux 是如何运作的
关于 Store:
- 整个应用只有一个唯一的 Store
- Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
- 状态树上的每个字段都可以进一步由不同的 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
// 规则匹配到的组件作为
props.children
传入const Container = (props) => {
return (
// route 规则:
// - /list
显示 List
组件
// - /item/:id
显示 Item
组件
const routes = (
export default routes;
从这里开始,我们通过这个非常简单的应用来解释实现服务器端渲染前后端涉及的一些细节问题。
Reducer
Store 是由 reducer 产生的,所以 reducer 实际上反映了 Store 的状态树结构
./reducers/index.js
return {
list: listReducer(state.list,action),item: itemReducer(state.item,action)
};
}
rootReducer 的 state 参数就是整个 Store 的状态树,状态树下的每个字段对应也可以有自己的reducer,所以这里引入了 listReducer 和 itemReducer,可以看到这两个 reducer的 state 参数就只是整个状态树上对应的 list 和 item 字段。
具体到 ./reducers/list.js
switch(action.type) {
case 'FETCH_LIST_SUCCESS': return [...action.payload];
default: return state;
}
}
list 就是一个包含 items 的简单数组,可能类似这种结构:[{ id: 0,name: 'first item'},{id: 1,name: 'second item'}],从 'FETCH_LIST_SUCCESS' 的 action.payload 获得。
然后是 ./reducers/item.js,处理获取到的 item 数据
switch(action.type) {
case 'FETCH_ITEM_SUCCESS': return [...action.payload];
default: return state;
}
}
Action
对应的应该要有两个 action 来获取 list 和 item,触发 reducer 更改 Store,这里我们定义 fetchList 和 fetchItem 两个 action。
./actions/index.js
return (dispatch) => {
return fetch('/api/list')
.then(res => res.json())
.then(json => dispatch({ type: 'FETCH_LIST_SUCCESS',payload: json }));
}
}
export function fetchItem(id) {
return (dispatch) => {
if (!id) return Promise.resolve();
return fetch(/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,把函数当作普通的 action dispatch 就好了,比如 dispatch(fetchList())
Store
我们用一个独立的 ./store.js,配置(比如 Apply Middleware)生成 Store
// ...
export default function configureStore(initialState) {
const store = createStore(rootReducer,initialState);
return store;
}
react-redux
接下来实现 ,
./app.js
__INITIAL_STATE__
来自服务器端渲染,下一部分细说const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const Root = (props) => {
return (
render(
至此,客户端部分结束。
Server Rendering
接下来的服务器端就比较简单了,获取数据可以调用 action,routes 在服务器端的处理参考 react-router server rendering,在服务器端用一个 match 方法将拿到的 request url 匹配到我们之前定义的 routes,解析成和客户端一致的 props 对象传递给组件。
./server.js
function renderFullPage(html,initialState) {
return `
<!DOCTYPE html>
<html lang="en">