为什么要实现服务端渲染(SSR)
总结下来有以下几点:
SEO,让搜索引擎更容易读取页面内容
首屏渲染速度更快(重点),无需等待js文件下载执行的过程
代码同构,服务端和客户端可以共享某些代码
今天我们将构建一个使用 Redux 的简单的 React 应用程序,实现服务端渲染(SSR)。该示例包括异步数据抓取,这使得任务变得更有趣。
如果您想使用本文中讨论的代码,请查看GitHub: answer518/react-redux-ssr
安装环境
在开始编写应用之前,需要我们先把环境编译/打包环境配置好,因为我们采用的是es6语法编写代码。我们需要将代码编译成es5代码在浏览器或node环境中执行。
我们将用babelify转换来使用browserify和watchify来打包我们的客户端代码。对于我们的服务器端代码,我们将直接使用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;