基于React+Redux的SSR实现方法

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

总结下来有以下几点:

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

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

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

安装环境

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

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

代码结构如下:

rush:plain;"> build src ├── client │ └── client.js └── server └── server.js

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

rush:js;"> "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服务器:

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

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

开发React+Redux应用

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

rush:js;"> [ { "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 处理过程如下:

rush:js;"> // 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 :

({ type: USERS_FETCHED,response });

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

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

export default () => createStore(reducer);

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

实现React组件

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

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

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

rush:js;"> // 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 (

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

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 重新计算状态,而我们的 由于连接到 Redux 从而被重新渲染。

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

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

运行Node Server

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

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(`

App
`); });

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

rush:xhtml;"> App
`); } });

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

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

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

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

rush:xhtml;"> App
Eve Holt

Charles Morris

Tracey Ramos