玩转 React 服务器端渲染

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 包含了几个方法比如 dispatchgetState 来处理数据流
  • 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

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)) 获取数据,显示详情页内容。

更多参考

时间: 2024-10-21 13:49:33

玩转 React 服务器端渲染的相关文章

基于React服务器端渲染的博客系统

系统目录及源码由此进入 目录 1. 开发前准备 1.1 技术选型1.2 整体设计1.3 构建开发 2. 技术点 2.1 react2.2 redux, react-router2.3 server-render 3. 总结 正文 1. 开发前准备 1. 1 技术选型 对于个人的博客系统而言,服务器计算能力往往不是需要考虑,而其中的 I/O 操作是比较复杂的,同理对前端的交互要求也是较高的,所以这次主要还是围绕 Node系 ,React系 框架进行开发.对于 2016 年后的互联网产品, Reac

服务器端渲染的特点

服务器端渲染固然耗CPU,但可以使用服务器端缓存的方式解决,并不是每个用户访问都需要重新渲染一遍.而且服务器端渲染甚至可以潜在地增加服务器效率(这点在参考资料第二个里有提到,不过是纯英文的,我有空会翻译下).2. 服务器端和客户端可以共享某些代码,避免重复定义.这样可以使结构更清晰,增加可维护性3. 首次加载页面的速度加快.客户端渲染的一个缺点是,当用户第一次进入站点,此时浏览器中没有缓存,需要下载代码后在本地渲染,时间较长.而服务器渲染则是,用户在下载的已经是渲染好的页面了,打开速度比本地渲染

【大前端之前后分离】JS前端渲染VS服务器端渲染

前言 之前看了一篇文章:@Charlie.Zheng Web系统开发构架再思考-前后端的完全分离,文中论述了为何要前后分离,站在前端的角度来看,是很有必要的:但是如何说服团队使用前端渲染方案却是一个现实问题,因为如果我是一个服务器端,我便会觉得不是很有必要,为什么要前后分离,前后分离后遗留了什么问题,如何解决,都得说清楚,这样才能说服团队使用前端渲染的方案,而最近我刚好遇到了框架选型的抉择. 来到新公司开始新项目了,需要做前端框架选型,因为之前内部同事采用的fis框架,而这边又是使用的php,这

服务器端渲染VS浏览器端渲染

1)浏览器渲染和服务器渲染区别:何为渲染?如果我们只是想显示一堆不变的数据,那么我们直接写一个a.html丢到服务器上让客户端访问就可以了.但这是基本不可能的事情,数据一般是变化的.你不可能为每套数据写一个视图,所以我们需要分离数据和视图,然后使用一种技术将数据塞到视图中,这种技术就叫渲染.这工作放在服务器上做就是服务器渲染,放在浏览器做就是浏览器渲染. 这里的渲染,就是指生成html文档的过程,和浏览器渲染html没有关系.浏览器端渲染,指的是用js去生成html,前端做路由.举例:React

编程学习之如何在Node.js中优化服务器端渲染?[图]

编程学习之如何在Node.js中优化服务器端渲染?[图]在 Airbnb,我们花了数年时间将所有前端代码迁移到 React 架构,Ruby on Rails 在 Web 应用中所占的比例每天都在减少.实际上,我们很快会转向另一个新的服务,即通过 Node.js 提供完整的服务器端渲染页面.这个服务将为 Airbnb 的所有产品渲染大部分 HTML.这个渲染引擎不同于其他后端服务,因为它不是用 Ruby 或 Java 开发的,但它也不同于常见的 I/O 密集型 Node.js 服务.一说起 Nod

React 服务器渲染原理解析与实践

第1章 服务器端渲染基础本章主要讲解客户端与服务器端渲染的概念,分析客户端渲染和服务器端渲染的利弊,带大家对服务器端渲染有一个粗浅认识. 1-1 课程导学1-2 什么是服务器端渲染1-3 什么是客户端渲染1-4 React 客户端渲染的优势与弊端第2章 React中的服务器端渲染本章将借助Node.js,Webpack等工具的帮助,带大家实现一个非常基础的基于React.js技术栈的服务器端渲染模型,过程中还会讲解虚拟DOM与服务器端渲染的内在联系. 2-1 在服务器端编写 React 组件2-

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

细说后端模板渲染.客户端渲染.node 中间层.服务器端渲染(ssr) 前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染.客户端渲染.node 中间层.服务器端渲染(ssr). 1. 后端模板渲染 前端与后端最初的渲染方式是后端模板渲染,就是由后端使用模板引擎渲染好 html 后,返回给前端,前端再用 js 去操作 dom 或者渲染其他动态的部分. 这个过程大致分成以下几个步骤: 前端请求一个地址 url 后端接收到这个请求,然后根据请求信息,从数据库或者其他地方获取相应的数据 使用

玩转 React(五)- 组件的内部状态和生命周期

文章标题总算是可以正常一点了-- 通过之前的文章我们已经知道:在 React 体系中所谓的 "在 JavaScript 中编写 HTML 代码" 指的是 React 扩展了 JavaScript 的语法,也就是 JSX.JSX 语法中可以以类似 HTML 语法的方式使用 React 组件,从而编写 React 组件就有一种创造一个新的 HTML 标签的体验. 上一篇文章<玩转 React(四)- 创造一个新的 HTML 标签>介绍了如何来创建一个 React 组件,以及组件

玩转 React(三)- JavaScript代码里写HTML一样可以很优雅

这是<玩转 React>系列的第三篇,看到本篇的标题,了解过 React 的同学可能已经大致猜到我要讲什么了,本篇中要讲的内容对于刚接触 React 的同学来说,可能有些难以接受,但希望你能坚持学下去,这是 Facebook 的前端大神们为前端开发做出的革命性创新. React 第一印象 废话不多说,先看一段代码: class HelloMessage extends React.Component { render() { return <div>Hello {this.prop