基于React+Redux的SSR实现方法

为什么要实现服务端渲染(SSR)

总结下来有以下几点:

  1. SEO,让搜索引擎更容易读取页面内容
  2. 首屏渲染速度更快(重点),无需等待js文件下载执行的过程
  3. 代码同构,服务端和客户端可以共享某些代码

今天我们将构建一个使用 Redux 的简单的 React 应用程序,实现服务端渲染(SSR)。该示例包括异步数据抓取,这使得任务变得更有趣。

如果您想使用本文中讨论的代码,请查看GitHub: answer518/react-redux-ssr

安装环境

在开始编写应用之前,需要我们先把环境编译/打包环境配置好,因为我们采用的是es6语法编写代码。我们需要将代码编译成es5代码在浏览器或node环境中执行。

我们将用babelify转换来使用browserifywatchify来打包我们的客户端代码。对于我们的服务器端代码,我们将直接使用babel-cli。

代码结构如下:

build
src
 ├── client
 │  └── client.js
 └── server
   └── server.js

我们在package.json里面加入以下两个命令脚本:

"scripts": {
  "build": "
   browserify ./src/client/client.js -o ./build/bundle.js -t babelify &&
   babel ./src/ --out-dir ./build/",
  "watch": "
   concurrently
    \"watchify ./src/client/client.js -o ./build/bundle.js -t babelify -v\"
    \"babel ./src/ --out-dir ./build/ --watch\"
   "
}

concurrently库帮助并行运行多个进程,这正是我们在监控更改时需要的。

最后一个有用的命令,用于运行我们的http服务器:

"scripts": {
 "build": "...",
 "watch": "...",
 "start": "nodemon ./build/server/server.js"
}

不使用 node ./build/server/server.js 而使用 Nodemon 的原因是,它可以监控我们代码中的任何更改,并自动重新启动服务器。这一点在开发过程会非常有用。

开发React+Redux应用

假设服务端返回以下的数据格式:

[
    {
      "id": 4,
      "first_name": "Gates",
      "last_name": "Bill",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"
    },
    {
      ...
    }
]

我们通过一个组件将数据渲染出来。在这个组件的 componentWillMount 生命周期方法中,我们将触发数据获取,一旦请求成功,我们将发送一个类型为 user_fetch 的操作。该操作将由一个 reducer 处理,我们将在 Redux 存储中获得更新。状态的改变将触发我们的组件重新呈现指定的数据。

Redux具体实现

reducer 处理过程如下:

// reducer.js
import { USERS_FETCHED } from './constants';

function getInitialState() {
 return { users: null };
}

const reducer = function (oldState = getInitialState(), action) {
 if (action.type === USERS_FETCHED) {
  return { users: action.response.data };
 }
 return oldState;
};

为了能派发 action 请求去改变应用状态,我们需要编写 Action Creator :

// actions.js
import { USERS_FETCHED } from './constants';
export const usersFetched = response => ({ type: USERS_FETCHED, response });

// selectors.js
export const getUsers = ({ users }) => users;

Redux 实现的最关键一步就是创建 Store :

// store.js
import { USERS_FETCHED } from './constants';
import { createStore } from 'redux';
import reducer from './reducer';

export default () => createStore(reducer);

为什么直接返回的是工厂函数而不是 createStore(reducer) ?这是因为当我们在服务器端渲染时,我们需要一个全新的 Store 实例来处理每个请求。

实现React组件

在这里需要提的一个重点是,一旦我们想实现服务端渲染,那我们就需要改变之前的纯客户端编程模式。

服务器端渲染,也叫代码同构,也就是同一份代码既能在客户端渲染,又能在服务端渲染。

我们必须保证代码能在服务端正常的运行。例如,访问 Window 对象,Node不提供Window对象的访问。

// App.jsx
import React from 'react';
import { connect } from 'react-redux';

import { getUsers } from './redux/selectors';
import { usersFetched } from './redux/actions';

const ENDPOINT = 'http://localhost:3000/users_fake_data.json';

class App extends React.Component {
 componentWillMount() {
  fetchUsers();
 }
 render() {
  const { users } = this.props;

  return (
   <div>
    {
     users && users.length > 0 && users.map(
      // ... render the user here
     )
    }
   </div>
  );
 }
}

const ConnectedApp = connect(
 state => ({
  users: getUsers(state)
 }),
 dispatch => ({
  fetchUsers: async () => dispatch(
   usersFetched(await (await fetch(ENDPOINT)).json())
  )
 })
)(App);

export default ConnectedApp;

你看到,我们使用 componentWillMount 来发送 fetchUsers 请求, componentDidMount 为什么不能用呢? 主要原因是 componentDidMount 在服务端渲染过程中并不会执行。

fetchUsers 是一个异步函数,它通过Fetch API请求数据。当数据返回时,会派发 users_fetch 动作,从而通过 reducer 重新计算状态,而我们的 <App /> 由于连接到 Redux 从而被重新渲染。

// client.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from './App.jsx';
import createStore from './redux/store';

ReactDOM.render(
 <Provider store={ createStore() }><App /></Provider>,
 document.querySelector('#content')
);

运行Node Server

为了演示方便,我们首选Express作为http服务器。

// server.js
import express from 'express';

const app = express();

// Serving the content of the "build" folder. Remember that
// after the transpiling and bundling we have:
//
// build
//  ├── client
//  ├── server
//  │  └── server.js
//  └── bundle.js
app.use(express.static(__dirname + '/../'));

app.get('*', (req, res) => {
 res.set('Content-Type', 'text/html');
 res.send(`
  <html>
   <head>
    <title>App</title>
   </head>
   <body>
    <div id="content"></div>
    <script src="/bundle.js"></script>
   </body>
  </html>
 `);
});

app.listen(
 3000,
 () => console.log('Example app listening on port 3000!')
);

有了这个文件,我们可以运行 npm run start 并访问 http://localhost:3000 。我们看到数据获取成功,并成功的显示了。

服务端渲染

目前为止,我们的服务端仅仅是返回了一个 html 骨架,而所有交互全在客户端完成。浏览器需要先下载 bundle.js 后执行。而服务端渲染的作用就是在服务器上执行所有操作并发送最终标记,而不是把所有工作交给浏览器执行。 React 足够的聪明,能够识别出这些标记。

还记得我们在客户端做的以下事情吗?

import ReactDOM from 'react-dom';

ReactDOM.render(
 <Provider store={ createStore() }><App /></Provider>,
 document.querySelector('#content')
);

服务端几乎相同:

import ReactDOMServer from 'react-dom/server';

const markupAsString = ReactDOMServer.renderToString(
 <Provider store={ store }><App /></Provider>
);

我们使用了相同的组件 <App /> 和 store ,不同之处在于它返回的是一个字符串,而不是虚拟DOM。

然后将这个字符串加入到 Express 的响应里面,所以服务端代码为:

const store = createStore();
const content = ReactDOMServer.renderToString(
 <Provider store={ store }><App /></Provider>
);

app.get('*', (req, res) => {
 res.set('Content-Type', 'text/html');
 res.send(`
  <html>
   <head>
    <title>App</title>
   </head>
   <body>
    <div id="content">${ content }</div>
    <script src="/bundle.js"></script>
   </body>
  </html>
 `);
});

如果重新启动服务器并打开相同的 http://localhost:3000 ,我们将看到以下响应:

<html>
 <head>
  <title>App</title>
 </head>
 <body>
  <div id="content"><div data-reactroot=""></div></div>
  <script src="/bundle.js"></script>
 </body>
</html>

我们的页面中确实有一些内容,但它只是 <div data-reactroot=""></div> 。这并不意味着程序出错了。这绝对是正确的。 React 确实呈现了我们的页面,但它只呈现静态内容。在我们的组件中,我们在获取数据之前什么都没有,数据的获取是一个异步过程,在服务器上呈现时,我们必须考虑到这一点。这就是我们的任务变得棘手的地方。这可以归结为我们的应用程序在做什么。在本例中,客户端代码依赖于一个特定的请求,但如果使用 redux-saga 库,则可能是多个请求,或者可能是一个完整的root saga。我意识到处理这个问题的两种方法:

1、我们明确知道请求的页面需要什么样的数据。我们获取数据并使用该数据创建 Redux 存储。然后我们通过提供已完成的 Store 来呈现页面,理论上我们可以做到。

2、我们完全依赖于运行在客户端上的代码,计算出最终的结果。

第一种方法,需要我们在两端做好状态管理。第二种方法需要我们在服务端使用一些额外的库或工具,来确保同一套代码能在服务端和客户端做相同的事情,我个人比较推荐使用这种方法。

例如,我们使用了 Fetch API 向后端发出异步请求,而服务端默认是不支持的。我们需要做的就是在 server.js 中将 Fetch 导入:

import 'isomorphic-fetch';

我们使用客户端API接收异步数据,一旦 Store 获取到异步数据,我们将触发 ReactDOMServer.renderToString 。它会提供给我们想要的标记。我们的Express处理器是这样的:

app.get('*', (req, res) => {
 const store = createStore();

 const unsubscribe = store.subscribe(() => {
  const users = getUsers(store.getState());

  if (users !== null && users.length > 0) {
   unsubscribe();

   const content = ReactDOMServer.renderToString(
    <Provider store={ store }><App /></Provider>
   );

   res.set('Content-Type', 'text/html');
   res.send(`
    <html>
     <head>
      <title>App</title>
     </head>
     <body>
      <div id="content">${ content }</div>
      <script src="/bundle.js"></script>
     </body>
    </html>
   `);
  }
 });

 ReactDOMServer.renderToString(<Provider store={ store }><App /></Provider>);
});

我们使用 Store subscribe 方法来监听状态。当状态发生变化——是否有任何用户数据被获取。如果 users 存在,我们将 unsubscribe() ,这样我们就不会让相同的代码运行两次,并且我们使用相同的存储实例转换为string。最后,我们将标记输出到浏览器。

store.subscribe方法返回一个函数,调用这个函数就可以解除监听

有了上面的代码,我们的组件已经可以成功地在服务器端渲染。通过开发者工具,我们可以看到发送到浏览器的内容:

<html>
     <head>
      <title>App</title>
      <style>
       body {
        font-size: 18px;
        font-family: Verdana;
       }
      </style>
     </head>
     <body>
      <div id="content"><div data-reactroot=""><p>Eve Holt</p><p>Charles Morris</p><p>Tracey Ramos</p></div></div>
      <script>
       window.__APP_STATE = {"users":[{"id":4,"first_name":"Eve","last_name":"Holt","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"},{"id":5,"first_name":"Charles","last_name":"Morris","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"},{"id":6,"first_name":"Tracey","last_name":"Ramos","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/bigmancho/128.jpg"}]};
      </script>
      <script src="/bundle.js"></script>
     </body>
    </html>

当然,现在并没有结束,客户端 JavaScript 不知道服务器上发生了什么,也不知道我们已经对API进行了请求。我们必须通过传递 Store 的状态来通知浏览器,以便它能够接收它。

const content = ReactDOMServer.renderToString(
 <Provider store={ store }><App /></Provider>
);

res.set('Content-Type', 'text/html');
res.send(`
 <html>
  <head>
   <title>App</title>
  </head>
  <body>
   <div id="content">${ content }</div>
   <script>
    window.__APP_STATE = ${ JSON.stringify(store.getState()) };
   </script>
   <script src="/bundle.js"></script>
  </body>
 </html>
`);

我们将 Store 状态放到一个全局变量 __APP_STATE 中, reducer 也有一点变化:

function getInitialState() {
 if (typeof window !== 'undefined' && window.__APP_STATE) {
  return window.__APP_STATE;
 }
 return { users: null };
}

注意 typeof window !== 'undefined' ,我们必须这样做,因为这段代码也会在服务端执行,这就是为什么说在做服务端渲染时要非常小心,尤其是全局使用的浏览器api的时候。

最后一个需要优化的地方,就是当已经取到 users 时,必须阻止 fetch 。

componentWillMount() {
 const { users, fetchUsers } = this.props;

 if (users === null) {
  fetchUsers();
 }
}

总结

服务器端呈现是一个有趣的话题。它有很多优势,并改善了整体用户体验。它还会提升你的单页应用程序的SEO。但这一切并不简单。在大多数情况下,需要额外的工具和精心选择的api。

这只是一个简单的案例,实际开发场景往往比这个复杂的多,需要考虑的情况也会非常多,你们的服务端渲染是怎么做的?

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

(0)

相关推荐

  • 详解React+Koa实现服务端渲染(SSR)

    React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点... 通过React, 我们通常会开发一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js, 当所有js加载完毕后,开始执行js, 最后再渲染到dom中, 在这个过程中,一般用户只能

  • 基于React+Redux的SSR实现方法

    为什么要实现服务端渲染(SSR) 总结下来有以下几点: SEO,让搜索引擎更容易读取页面内容 首屏渲染速度更快(重点),无需等待js文件下载执行的过程 代码同构,服务端和客户端可以共享某些代码 今天我们将构建一个使用 Redux 的简单的 React 应用程序,实现服务端渲染(SSR).该示例包括异步数据抓取,这使得任务变得更有趣. 如果您想使用本文中讨论的代码,请查看GitHub: answer518/react-redux-ssr 安装环境 在开始编写应用之前,需要我们先把环境编译/打包环境

  • React/Redux应用使用Async/Await的方法

    Async/Await是尚未正式公布的ES7标准新特性.简而言之,就是让你以同步方法的思维编写异步代码.对于前端,异步任务代码的编写经历了 callback 到现在流行的 Promise ,最终会进化为 Async/Await .虽然这个特性尚未正式发布,但是利用babel polyfill我们已经可以在应用中使用它了. 现在假设一个简单的React/Redux应用,我将引入 Async/Await 到其代码. Actions 此例子中有一个创建新文章的 Action ,传统方法是利用 Prom

  • 一文详解React Redux使用方法

    目录 一.理解JavaScript纯函数 1.1 纯函数的概念 1.2 副作用概念的理解 1.3 纯函数在函数式编程的重要性 二.Redux的核心思想 2.1 为什么需要 Redux 2.2 Redux的核心概念 2.2.1 store 2.2.2 action 2.2.3 reducer 2.3 Redux的三大原则 2.3.1 单一数据源 2.3.2 State是只读的 2.3.3 使用纯函数来执行修改 2.4 Redux 工作流程 三.Redux基本使用 3.1 创建Store的过程 3.

  • 基于React Hooks的小型状态管理详解

    目录 实现基于 React Hooks 的状态共享 使用感受 本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式. 实现基于 React Hooks 的状态共享 React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux.MobX 等.这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度. 实际上很多时候,我不想定义 mutation 和

  • 详解超简单的react服务器渲染(ssr)入坑指南

    前言 本文是基于react ssr的入门教程,在实际项目中使用还需要做更多的配置和优化,比较适合第一次尝试react ssr的小伙伴们.技术涉及到 koa2 + react,案例使用create-react-app创建 SSR 介绍 Server Slide Rendering,缩写为 ssr 即服务器端渲染,这个要从SEO说起,目前react单页应用HTML代码是下面这样的 <!DOCTYPE html> <html lang="en"> <head&g

  • 基于React Context实现一个简单的状态管理的示例代码

    目录 前言 封装一个父组件用来包裹其他子组件 子组件如何获取数据呢 class Component 方式 context.Consumer useContext 总结 参考 前言 在大多数情况下,我们开发项目都需要一个状态管理,方便我们在全局共享状态库,在React生态里比较流行的几个库 redux.mobx.recoil 但是对于小项目,我们完全可以自己封装一个状态管理,减少一个包的安装就可以减小打包以后的项目体积. 主要分两步: 封装一个顶层组件提供数据 子组件获取数据和更新数据 封装一个父

  • React Redux应用示例详解

    目录 一 React-Redux的应用 1.学习文档 2.Redux的需求 3.什么是Redux 4.什么情况下需要使用redux 二.最新React-Redux 的流程 安装Redux Toolkit 创建一个 React Redux 应用 基础示例 Redux Toolkit 示例 三.使用教程 安装Redux Toolkit和React-Redux​ 创建 Redux Store​ 为React提供Redux Store​ 创建Redux State Slice​ 将Slice Reduc

  • 基于react框架使用的一些细节要点的思考

    这篇文章主要是写关于学习react中的一些自己的思考: 1.setState到底是同步的还是异步的? 2.如何在子组件中改变父组件的state 3.context的运用,避免"props传递地狱" 4.组件类里有私有变量a,它到底改放在this.a中还是this.state对象中(作为属性a)呢? 1.setState到底是同步的还是异步的? class MyComponent extends React.Component{ constructor(props) { super(pr

  • 基于react后端渲染模板引擎noox发布使用

    React 组件化思想受到越来越多开发者的关注,组件化思想帮助开发者将页面解耦成一个一个组件,代码更加模块化, 更易扩展.而目前流行的后端模板引擎如 ejs, swig, jade, art 共同的问题是: 需要学习各类模板引擎定义的语法,如 {{if}}, {{loop}} 对组件化支持不够强,实现复杂,不易用 针对以上痛点,笔者基于 React 造出了 noox 这样一个工具,专注于后端模板的解析,让模板解析更加简单,易用. 使用方法 安装 npm install noox 简单的 demo

  • webpack+react+antd脚手架优化的方法

    在之前的文章中了解了webpack v4的特性,现在将知识用于实践,对之前写的一个react脚手架进行打包性能的优化. 优化类目 样式分离 第三方资源分离 区分开发环境 热更新 提取公共代码 1. CSS分离 npm install extract-text-webpack-plugin -D webpack.config.js 将css.less.sass文件单独从打包文件中分离 + let cssExtract = new ExtractTextWebpackPlugin({ + filen

随机推荐