为什么要实现服务端渲染(SSR)
总结下来有以下几点:
今天我们将构建一个使用 Redux 的简单的 React 应用程序,实现服务端渲染(SSR)。该示例包括异步数据抓取,这使得任务变得更有趣。
如果您想使用本文中讨论的代码,请查看GitHub: @L_301_0@
安装环境
在开始编写应用之前,需要我们先把环境编译/打包环境配置好,因为我们采用的是es6语法编写代码。我们需要将代码编译成es5代码在浏览器或node环境中执行。
我们将用babelify转换来使用browserify和watchify来打包我们的客户端代码。对于我们的服务器端代码,我们将直接使用babel-cli。
代码结构如下:
我们在package.json里面加入以下两个命令脚本:
concurrently库帮助并行运行多个进程,这正是我们在监控更改时需要的。
最后一个有用的命令,用于运行我们的http服务器:
不使用 node ./build/server/server.js
而使用 Nodemon
的原因是,它可以监控我们代码中的任何更改,并自动重新启动服务器。这一点在开发过程会非常有用。
开发React+Redux应用
假设服务端返回以下的数据格式:
我们通过一个组件将数据渲染出来。在这个组件的 componentWillMount 生命周期方法中,我们将触发数据获取,一旦请求成功,我们将发送一个类型为 user_fetch 的操作。该操作将由一个 reducer 处理,我们将在 Redux 存储中获得更新。状态的改变将触发我们的组件重新呈现指定的数据。
Redux具体实现
reducer 处理过程如下:
return { users: null };
}
const reducer = function (oldState = getInitialState(),action) {
if (action.type === USERS_FETCHED) {
return { users: action.response.data };
}
return oldState;
};
为了能派发 action 请求去改变应用状态,我们需要编写 Action Creator :
// selectors.js
export const getUsers = ({ users }) => users;
Redux 实现的最关键一步就是创建 Store :
为什么直接返回的是工厂函数而不是 createStore(reducer) ?这是因为当我们在服务器端渲染时,我们需要一个全新的 Store 实例来处理每个请求。
实现React组件
在这里需要提的一个重点是,一旦我们想实现服务端渲染,那我们就需要改变之前的纯客户端编程模式。
服务器端渲染,也叫代码同构,也就是同一份代码既能在客户端渲染,又能在服务端渲染。
我们必须保证代码能在服务端正常的运行。例如,访问 Window 对象,Node不提供Window对象的访问。
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 (
const ConnectedApp = connect(
state => ({
users: getUsers(state)
}),dispatch => ({
fetchUsers: async () => dispatch(
usersFetched(await (await fetch(ENDPOINT)).json())
)
})
)(App);
export default ConnectedApp;
你看到,我们使用 compon
entWillMount 来发送 fetchUsers
请求, componentDidMount
为什么不能用呢? 主要原因是 componentDidMount
在服务端渲染过程中并不会执行。
fetchUsers 是一个异步函数,它通过Fetch API请求数据。当数据返回时,会派发 users_fetch 动作,从而通过 reducer 重新计算状态,而我们的
import createStore from './redux/store';
ReactDOM.render(
<Provider store={ createStore() }>
);
运行Node Server
为了演示方便,我们首选Express作为http服务器。
// 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(`
如果重新启动服务器并打开相同的 http://localhost:3000 ,我们将看到以下响应: