基于React+Redux的SSR实现

2018-07-13 admin

为什么要实现服务端渲染(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](/go/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FFetch_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>);
});

我们使用Storesubscribe方法来监听状态。当状态发生变化——是否有任何用户数据被获取。如果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。

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

如果有人让你推荐前端技术书,请让他看这个列表 ->《经典前端技术书籍》 <h10 id=“94909votetotal”>2</h10> 赞 1 收藏 评论

原文链接:http://web.jobbole.com/94909/

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-35941.html

文章标题:基于React+Redux的SSR实现

相关文章
Android中Okhttp3实现上传多张图片同时传递参数
之前上传图片都是直接将图片转化为io流传给服务器,没有用框架传图片。 最近做项目,打算换个方法上传图片。 Android发展到现在,Okhttp显得越来越重要,所以,这次我选择用Okhttp上传图片。 Okhttp目前已经更新到Okhttp...
2017-03-17
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
React.js编程思想
JavaScript框架层出不穷,在很多程序员看来,React.js是创建大型、快速的Web应用的最好方式。这一款由Facebook出品的JS框架,无论是在Facebook还是在Instagram中,它的表现都非常出色。 使用React.j...
2015-11-12
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
Vue.js组件tab实现选项卡切换
本文实例为大家分享了vue插件tab选项卡的具体代码,供大家参考,具体内容如下 效果图: 代码如下: &lt;!DOCTYPE html&gt; &lt;html lang=&quot;en&quot;&gt; &lt;head&gt; ...
2017-03-13
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
vue+element-ui+slot-scope实现可编辑表格
1.咱开发拿到需求大多数是去网上找成型的组件,找不到再看原生的方法能否实现,大牛除外哈,大牛一般喜欢封装组件框架。 2.可编辑表格在后台管理系统还是比较常用的,因为比较流行框架element,iview都没有这个应用,所以考虑了两种方法,下...
2017-12-25
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
回到顶部