玩转 React 服务器端渲染

React 提供了两个方法 renderToString 和 renderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,因此让服务器端渲染变成了一件有吸引力的事情。php

服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:css

  • 先后端能够共享代码
  • 先后端路由能够统一处理

React 生态提供了不少选择方案,这里咱们选用 Redux 和 react-router 来作说明。html

Redux

Redux 提供了一套相似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。react

2 分钟了解 Redux 是如何运做的

关于 Store:git

  • 整个应用只有一个惟一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每一个字段均可以进一步由不一样的 reducer 函数生成
  • Store 包含了几个方法好比 dispatchgetState 来处理数据流
  • Store 的状态树只能由 dispatch(action) 来触发更改

Redux 的数据流:github

  • action 是一个包含 type, payload } 的对象
  • reducer 函数经过 store.dispatch(action) 触发
  • reducer 函数接受 (state, action) 两个参数,返回一个新的 state
  • reducer 函数判断 action.type 而后处理对应的 action.payload 数据来更新状态树

因此对于整个应用来讲,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 renderToString 就将整个应用输出成包含了初始化数据的 HTML。express

react-router

react-router 经过一种声明式的方式匹配不一样路由决定在页面上展现不一样的组件,而且经过 props 将路由信息传递给组件使用,因此只要路由变动,props 就会变化,触发组件 re-render。json

假设有一个很简单的应用,只有两个页面,一个列表页 /list 和一个详情页 /item/:id,点击列表上的条目进入详情页。redux

能够这样定义路由,./routes.js后端

import React from 'react';
import { Route } from 'react-router';
import { List, Item } from './components';

// 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
// 规则匹配到的组件做为 `props.children` 传入 const Container = (props) => { return (  <div>{props.children}</div> ); }; // route 规则: // - `/list` 显示 `List` 组件 // - `/item/:id` 显示 `Item` 组件 const routes = ( <Route path="/" component={Container} >  <Route path="list" component={List} />  <Route path="item/:id" component={Item} /> </Route> ); export default routes; 

从这里开始,咱们经过这个很是简单的应用来解释实现服务器端渲染先后端涉及的一些细节问题。

Reducer

Store 是由 reducer 产生的,因此 reducer 实际上反映了 Store 的状态树结构

./reducers/index.js

import listReducer from './list';
import itemReducer from './item';

export default function rootReducer(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

const initialState = [];

export default function listReducer(state = initialState, action) {
  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 数据

const initialState = {};

export default function listReducer(state = initialState, action) {
  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

import fetch from 'isomorphic-fetch';

export function fetchList() {
  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

import { createStore } from 'redux';
import rootReducer from './reducers';

// Apply middleware here
// ...

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState);
  return store;
}

react-redux

接下来实现 <List><Item> 组件,而后把 redux 和 react 组件关联起来,具体细节参见 react-redux

./app.js

import React from 'react';
import { render } from 'react-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

// `__INITIAL_STATE__` 来自服务器端渲染,下一部分细说 const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); const Root = (props) => { return (  <div>  <Provider store={store}>  <Router history={createBrowserHistory()}>  {routes}  </Router>  </Provider>  </div> ); } render(<Root />, document.getElementById('root')); 

至此,客户端部分结束。

Server Rendering

接下来的服务器端就比较简单了,获取数据能够调用 action,routes 在服务器端的处理参考 react-router server rendering,在服务器端用一个 match 方法将拿到的 request url 匹配到咱们以前定义的 routes,解析成和客户端一致的 props 对象传递给组件。

./server.js

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RoutingContext, match } from 'react-router';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

const app = express();

function renderFullPage(html, initialState) {
  return `
 <!DOCTYPE html>  <html lang="en">  <head>  <meta charset="UTF-8">  </head>  <body>  <div id="root">  <div>  ${html}  </div>  </div>  <script>  window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};  </script>  <script src="/static/bundle.js"></script>  </body>  </html> `; } app.use((req, res) => { match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {  if (err) {  res.status(500).end(`Internal Server Error ${err}`);  } else if (redirectLocation) {  res.redirect(redirectLocation.pathname + redirectLocation.search);  } else if (renderProps) {  const store = configureStore();  const state = store.getState();  Promise.all([  store.dispatch(fetchList()),  store.dispatch(fetchItem(renderProps.params.id))  ])  .then(() => {  const html = renderToString(  <Provider store={store}>  <RoutingContext {...renderProps} />  </Provider>  );  res.end(renderFullPage(html, store.getState()));  });  } else {  res.status(404).end('Not found');  } }); }); 

服务器端渲染部分能够直接经过共用客户端 store.dispatch(action) 来统一获取 Store 数据。另外注意 renderFullPage 生成的页面 HTML 在 React 组件 mount 的部分(<div id="root">),先后端的 HTML 结构应该是一致的。而后要把 store 的状态树写入一个全局变量(__INITIAL_STATE__),这样客户端初始化 render 的时候可以校验服务器生成的 HTML 结构,而且同步到初始化状态,而后整个页面被客户端接管。

最后关于页面内连接跳转如何处理?

react-router 提供了一个 <Link> 组件用来替代 <a> 标签,它负责管理浏览器 history,从而不是每次点击连接都去请求服务器,而后能够经过绑定 onClick 事件来做其余处理。

好比在 /list 页面,对于每个 item 都会用 <Link> 绑定一个 route url:/item/:id,而且绑定 onClick 去触发 dispatch(fetchItem(id)) 获取数据,显示详情页内容。

更多参考

相关文章
相关标签/搜索