当前位置: 移动技术网 > IT编程>开发语言>JavaScript > 基于React+Redux的SSR实现方法

基于React+Redux的SSR实现方法

2018年08月05日  | 移动技术网IT编程  | 我要评论

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

总结下来有以下几点:

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

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

如果您想使用本文中讨论的代码,请查看github:

安装环境

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

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

代码结构如下:

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\"
   "
}

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

最后一个有用的命令,用于运行我们的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。

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

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网