浅谈React 服务器端渲染的使用


Posted in Javascript onMay 08, 2018

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

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

  1. 前后端可以共享代码
  2. 前后端路由可以统一处理

React 生态提供了很多选择方案,这里我们选用 Redux 和 react-router 来做说明。

Redux

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

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

关于 Store:

  1. 整个应用只有一个唯一的 Store
  2. Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  3. 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  4. Store 包含了几个方法比如 dispatch, getState 来处理数据流
  5. Store 的状态树只能由 dispatch(action) 来触发更改

Redux 的数据流:

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

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

react-router

react-router 通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过 props 将路由信息传递给组件使用,所以只要路由变更,props 就会变化,触发组件 re-render。

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

可以这样定义路由,./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)) 获取数据,显示详情页内容。

更多参考

Universal (Isomorphic)
isomorphic-redux-app

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
从javascript语言本身谈项目实战
Dec 27 Javascript
JQuery 网站换肤功能实现代码
Nov 02 Javascript
JS小功能(checkbox实现全选和全取消)实例代码
Nov 28 Javascript
js控制href内容的连接内容的变化示例
Apr 30 Javascript
JavaScript类继承及实例化的方法
Jul 25 Javascript
javascript运算符——逻辑运算符全面解析
Jun 27 Javascript
JavaScript省市区三级联动菜单效果
Sep 21 Javascript
Move.js入门
Feb 08 Javascript
JS数据类型STRING使用实例解析
Dec 18 Javascript
浅谈js中的attributes和Attribute的用法与区别
Jul 16 Javascript
js数组中去除重复值的几种方法
Aug 03 Javascript
vue 单页应用和多页应用的优劣
Oct 22 Javascript
vue.js做一个简单的编辑菜谱功能
May 08 #Javascript
webstorm和.vue中es6语法报错的解决方法
May 08 #Javascript
vue2.0实现移动端的输入框实时检索更新列表功能
May 08 #Javascript
webstorm添加*.vue文件支持
May 08 #Javascript
浅谈vue项目如何打包扔向服务器
May 08 #Javascript
Javascript实现购物车功能的详细代码
May 08 #Javascript
vue-cli 如何打包上线的方法示例
May 08 #Javascript
You might like
SONY SRF-M100的电路分析
2021/03/02 无线电
Smarty+QUICKFORM小小演示
2007/02/25 PHP
让PHP开发者事半功倍的十大技巧小结
2010/04/20 PHP
函数中使用require_once问题深入探讨 优雅的配置文件定义方法推荐
2014/07/02 PHP
PHP将进程作为守护进程的方法
2015/03/19 PHP
详解PHP中的PDO类
2015/07/06 PHP
PHP批量删除jQuery操作
2017/07/23 PHP
php实现的统计字数函数定义与使用示例
2017/07/26 PHP
因str_replace导致的注入问题总结
2019/08/08 PHP
jQuery的实现原理的模拟代码 -1 核心部分
2010/08/01 Javascript
bootstrap网页框架的使用方法
2016/05/10 Javascript
基于vuejs实现一个todolist项目
2017/04/11 Javascript
jquery dataTable 获取某行数据
2017/05/05 jQuery
gulp解决跨域的配置文件问题
2017/06/08 Javascript
vue2.0s中eventBus实现兄弟组件通信的示例代码
2017/10/25 Javascript
jQuery中复合选择器简单用法示例
2018/03/31 jQuery
解决vue项目中type=”file“ change事件只执行一次的问题
2018/05/16 Javascript
微信小程序以7天为周期连续签到7天功能效果的示例代码
2020/08/20 Javascript
Vue页面跳转传递参数及接收方式
2020/09/09 Javascript
[01:42:49]DOTA2-DPC中国联赛 正赛 iG vs PSG.LGD BO3 第一场 2月26日
2021/03/11 DOTA
python3模拟百度登录并实现百度贴吧签到示例分享(百度贴吧自动签到)
2014/02/24 Python
动感网页相册 python编写简单文件夹内图片浏览工具
2016/08/17 Python
asyncio 的 coroutine对象 与 Future对象使用指南
2016/09/11 Python
python 处理dataframe中的时间字段方法
2018/04/10 Python
使用Python写一个量化股票提醒系统
2018/08/22 Python
numpy下的flatten()函数用法详解
2019/05/27 Python
python脚本当作Linux中的服务启动实现方法
2019/06/28 Python
Pytorch中index_select() 函数的实现理解
2019/11/19 Python
基于Tensorflow批量数据的输入实现方式
2020/02/05 Python
详解Python设计模式之策略模式
2020/06/15 Python
python入门:argparse浅析 nargs='+'作用
2020/07/12 Python
kmart凯马特官网:美国最大的打折零售商和全球最大的批发商之一
2016/11/17 全球购物
给客户的感谢信
2015/01/21 职场文书
出国留学英文自荐信
2015/03/25 职场文书
单位更名证明
2015/06/18 职场文书
二十年同学聚会致辞
2015/07/28 职场文书