浅谈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 相关文章推荐
DOM_window对象属性之--clipboardData对象操作代码
Feb 03 Javascript
js获取当前地址 JS获取当前URL的示例代码
Feb 26 Javascript
jQuery中get和post方法传值测试及注意事项
Aug 08 Javascript
Vue.js学习之过滤器详解
Jan 22 Javascript
jquery拼接ajax 的json和字符串拼接的方法
Mar 11 Javascript
Vue.use源码学习小结
Jun 20 Javascript
使用elementUI实现将图片上传到本地的示例
Sep 04 Javascript
在vue项目中集成graphql(vue-ApolloClient)
Sep 08 Javascript
使用form-create动态生成vue自定义组件和嵌套表单组件
Jan 18 Javascript
vue实现在v-html的html字符串中绑定事件
Oct 28 Javascript
浅析JavaScript 函数柯里化
Sep 08 Javascript
详解vue-router的导航钩子(导航守卫)
Nov 02 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
PHP的类 功能齐全的发送邮件类
2006/10/09 PHP
PHP批量上传图片的具体实现方法介绍.
2014/02/26 PHP
PHP实现CSV文件的导入和导出类
2015/03/24 PHP
Opacity.js
2007/01/22 Javascript
jQuery 学习 几种常用方法
2009/06/11 Javascript
基于jquery实现的可以编辑选择的下拉框的代码
2010/11/19 Javascript
Event altKey,ctrlKey,shiftKey属性解析
2013/12/18 Javascript
JS简单操作select和dropdownlist实例
2014/11/26 Javascript
javascript中的this详解
2014/12/08 Javascript
对JavaScript中this指针的新理解分享
2015/01/31 Javascript
JS for...in 遍历语句用法实例分析
2016/08/24 Javascript
Bootstrap树形菜单插件TreeView.js使用方法详解
2016/11/01 Javascript
BootStrap中
2016/12/10 Javascript
jQuery日程管理插件fullcalendar使用详解
2017/01/07 Javascript
jquery PrintArea 实现票据的套打功能(代码)
2017/03/17 Javascript
npm国内镜像 安装失败的几种解决方案
2017/06/04 Javascript
Three.js中网格对象MESH的属性与方法详解
2017/09/27 Javascript
layui table 复选框跳页后再回来保持原来选中的状态示例
2019/10/26 Javascript
vue 解决无法对未定义的值,空值或基元值设置反应属性报错问题
2020/07/31 Javascript
关于你不想知道的所有Python3 unicode特性
2014/11/28 Python
举例讲解Python中的迭代器、生成器与列表解析用法
2016/03/20 Python
Python反射用法实例简析
2017/12/22 Python
Python实现的远程登录windows系统功能示例
2018/06/21 Python
Python代理IP爬虫的新手使用教程
2019/09/05 Python
Python 异步协程函数原理及实例详解
2019/11/13 Python
无惧面试,带你搞懂python 装饰器
2020/08/17 Python
Skyscanner英国:苏格兰的全球三大领先航班搜索服务之一
2017/11/09 全球购物
List, Set, Map是否继承自Collection接口?
2016/05/16 面试题
什么是Assembly(程序集)
2014/09/14 面试题
工商管理专业实习大学生自我鉴定
2013/09/19 职场文书
写给保洁员表扬信
2014/01/08 职场文书
教师暑期培训感言
2014/08/15 职场文书
民主生活会整改措施(党员)
2014/09/18 职场文书
2015年统计员个人工作总结
2015/07/23 职场文书
超市主管竞聘书
2015/09/15 职场文书
Python下opencv库的安装过程及问题汇总
2021/06/11 Python