总结下来有如下几点:javascript
今天咱们将构建一个使用Redux
的简单的React
应用程序,实现服务端渲染(SSR)。该示例包括异步数据抓取,这使得任务变得更有趣。css
若是您想使用本文中讨论的代码,请查看GitHub: answer518/react-redux-ssrhtml
在开始编写应用以前,须要咱们先把环境编译/打包环境配置好,由于咱们采用的是es6语法编写代码。咱们须要将代码编译成es5代码在浏览器或node环境中执行。java
咱们将用babelify转换来使用browserify和watchify来打包咱们的客户端代码。对于咱们的服务器端代码,咱们将直接使用babel-cli。node
代码结构以下:react
build
src
├── client
│ └── client.js
└── server
└── server.js
复制代码
咱们在package.json里面加入如下两个命令脚本:git
"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库帮助并行运行多个进程,这正是咱们在监控更改时须要的。es6
最后一个有用的命令,用于运行咱们的http服务器:github
"scripts": {
"build": "...",
"watch": "...",
"start": "nodemon ./build/server/server.js"
}
复制代码
不使用node ./build/server/server.js
而使用Nodemon
的缘由是,它能够监控咱们代码中的任何更改,并自动从新启动服务器。这一点在开发过程会很是有用。express
假设服务端返回如下的数据格式:
[
{
"id": 4,
"first_name": "Gates",
"last_name": "Bill",
"avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"
},
{
...
}
]
复制代码
咱们经过一个组件将数据渲染出来。在这个组件的componentWillMount
生命周期方法中,咱们将触发数据获取,一旦请求成功,咱们将发送一个类型为user_fetch
的操做。该操做将由一个reducer
处理,咱们将在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
实例来处理每一个请求。
在这里须要提的一个重点是,一旦咱们想实现服务端渲染,那咱们就须要改变以前的纯客户端编程模式。
服务器端渲染,也叫代码同构,也就是同一份代码既能在客户端渲染,又能在服务端渲染。
咱们必须保证代码能在服务端正常的运行。例如,访问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')
);
复制代码
为了演示方便,咱们首选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。我意识处处理这个问题的两种方法:
一、咱们明确知道请求的页面须要什么样的数据。咱们获取数据并使用该数据建立Redux
存储。而后咱们经过提供已完成的Store
来呈现页面,理论上咱们能够作到。
二、咱们彻底依赖于运行在客户端上的代码,计算出最终的结果。
第一种方法,须要咱们在两端作好状态管理。第二种方法须要咱们在服务端使用一些额外的库或工具,来确保同一套代码能在服务端和客户端作相同的事情,我我的比较推荐使用这种方法。
例如,咱们使用了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。
这只是一个简单的案例,实际开发场景每每比这个复杂的多,须要考虑的状况也会很是多,大家的服务端渲染是怎么作的?