浅谈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 相关文章推荐
基于jquery的文本框与autocomplete结合使用(asp.net+json)
May 30 Javascript
前台js改变Session的值(用ajax实现)
Dec 28 Javascript
用JQuery 判断某个属性是否存在hasAttr的解决方法
Apr 26 Javascript
javascript实现window.print()去除页眉页脚
Dec 30 Javascript
javascript中SetInterval与setTimeout的定时器用法
Aug 24 Javascript
ReactJs快速入门教程(精华版)
Nov 28 Javascript
浅谈Vue.js
Mar 02 Javascript
jquery实现图片轮播器
May 23 jQuery
Three.js 再探 - 写一个微信跳一跳极简版游戏
Jan 04 Javascript
基于vue 动态加载图片src的解决方法
Feb 05 Javascript
微信小程序实现下拉菜单切换效果
Mar 30 Javascript
vue组件添加事件@click.native操作
Oct 30 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
mysql总结之explain
2012/02/27 PHP
LaravelS通过Swoole加速Laravel/Lumen详解
2018/03/02 PHP
javascript日期转换 时间戳转日期格式
2011/11/05 Javascript
js Form.elements[i]的使用实例
2011/11/13 Javascript
javascript定时变换图片实例代码
2013/03/17 Javascript
JavaScript 创建运动框架的实现代码
2013/05/08 Javascript
JavaScript判断变量是否为undefined的两种写法区别
2013/12/04 Javascript
Nodejs Express4.x开发框架随手笔记
2015/11/23 NodeJs
JavaScript位置与大小(1)之正确理解和运用与尺寸大小相关的DOM属性
2015/12/26 Javascript
使用jQuery判断Div是否在可视区域的方法 判断div是否可见
2016/02/17 Javascript
实例解析angularjs的filter过滤器
2016/12/14 Javascript
详解JS中遍历语法的比较
2017/04/07 Javascript
JS获取浏览器地址栏的多个参数值的任意值实例代码
2018/07/24 Javascript
深入理解Vue 的钩子函数
2018/09/05 Javascript
vue根据值给予不同class的实例
2018/09/29 Javascript
Windows下支持自动更新的Electron应用脚手架的方法
2018/12/24 Javascript
使用ThinkJs搭建微信中控服务的实现方法
2019/08/08 Javascript
layui多iframe页面控制定时器运行的方法
2019/09/05 Javascript
vue跳转同一个组件,参数不同,页面接收值只接收一次的解决方法
2019/11/05 Javascript
JavaScript常用进制转换及位运算实例解析
2020/10/14 Javascript
Python中使用Inotify监控文件实例
2015/02/14 Python
python得到一个excel的全部sheet标签值方法
2018/12/10 Python
Python 网络编程之UDP发送接收数据功能示例【基于socket套接字】
2019/10/11 Python
vim自动补全插件YouCompleteMe(YCM)安装过程解析
2019/10/21 Python
浅谈Django QuerySet对象(模型.objects)的常用方法
2020/03/28 Python
python matlab库简单用法讲解
2020/12/31 Python
一款纯css3实现的鼠标悬停动画按钮
2014/12/29 HTML / CSS
CSS3 实现飘动的云朵动画
2020/12/01 HTML / CSS
美国在线家装零售商:Build.com
2016/09/02 全球购物
公司综合部的成员自我评价分享
2013/11/05 职场文书
个人生活学习自我评价范文
2013/11/26 职场文书
精彩的演讲稿开头
2014/05/08 职场文书
煤矿安全协议书
2014/08/20 职场文书
早上好问候语大全
2015/11/10 职场文书
js实现自动锁屏功能
2021/06/02 Javascript
业余无线电通联Q语
2022/02/18 无线电