React Server Rendering
得益于 virtual DOM 和 jsx, React 并不需要依赖于 DOM, 所以能在服务器上渲染 React 应用, 并且向客户端发送 HTML 代码.
Babel es6+
Node 目前只支持部分 es6 特性,我们需要 Babel 把代码转换一次,新建 .babelrc
文件
{
"presets": ["es2015", "react", "stage-0"],
"plugins": ["babel-plugin-transform-decorators-legacy"]
}
为了自动转换代码,我们使用 gulp
const gulp = require('gulp');
const babel = require('gulp-babel');
gulp.task("transform", () => {
return gulp.src("app/**/*.js")
.pipe(babel({
presets: ["es2015", "react", "stage-0"],
plugins: ["babel-plugin-transform-decorators-legacy"]
}))
.pipe(gulp.dest("build"));
});
gulp.task("watch", ()=> {
gulp.watch("app/**/*.js", ["transform"]);
});
gulp.task("default", ["transform", "watch"]);
Async
React 在 server 中的 Lifecycle 有些不一样, 因为服务器对客户端是单向的,这样基本上只会出输出我们在 jsx 中定义好的 markup 代码,异步请求的数据并不会被渲染,所以我们需要定义一些钩子,让异步请求完成后再向客户端发送数据。
React + Redux + React Router + immutable 应该是现在最稳定和常见的 React 应用组合,下面将以这个为例子:
Middleware
在 redux 中加入 promise middleware, 自动处理含有 promise 的 action:
export default function promiseMiddleware() {
return (next) => (action) => {
const {promise, type, ...rest} = action;
if(!promise) {
return next(action);
}
const SUCCESS = type + "_SUCCESS";
const REQUEST = type + "_PENDING";
const FAILURE = type + "_FAILURE";
next({...rest, type: REQUEST});
return promise.then((result) => {
const data = result && result.data ? result.data : result;
next({...rest, data, type: SUCCESS})
}).catch((error)=> {
console.log(error);
next({...rest, error, type: FAILURE})
});
}
}
Action
在 action 中添加 promise:
import request from "axios";
export const GET_POST = "GET_POST";
export function getPost(params) {
return {
type: GET_POST,
promise: request.get(`/posts/${params.id}`)
}
}
Reducer
middleware 会自动添加 promise 中各种状态的 action type, 所以在 reducer 中直接处理:
import {GET_POST} from "../actions/post";
import {Map, List, fromJS} from "immutable";
const initState = Map({
post: Map(),
isFetching: false
});
export default (state=initState, action) => {
switch (action.type) {
case `${GET_POST}_PENDING`:
return state.set("isFetching", true);
case `${GET_POST}_SUCCESS`:
return state.set("isFetching", false).set("post", fromJS(action.data));
default:
return state;
}
};
Container
在 Container 中加入 promises 钩子:
import React, {Component} from "react";
import {connect} from "react-redux";
import {getPost} from "../actions/post";
@connect(state => {
return {
post: state.post.get("post")
}
})
export default class Post extends Component {
constructor(props) {
super(props);
}
static promises = [
getPost
]
render() {
const {post} = this.props;
return (
<div>
<h1>{post.get("title")}</h1>
<div>{post.get("content")}</div>
</div>
);
}
}
Server
在当前 route component 的所有 promise resolve 后才会向客户端发送数据
import { renderToString } from "react-dom/server";
import { RouterContext } from "react-router";
import { Provider } from "react-redux";
function fetchComponentData(dispatch, components, params) {
const promises = components.reduce((prev, current) => {
return (current && current.promises || [])
.concat(current && current.WrappedComponent ? current.WrappedComponent.promises : [] || [])
.concat(prev || []);
}, []);
const fetch = promises.map(promise => dispatch(promise(params)));
return Promise.all(fetch);
}
app.use("/", function(req, res) {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
console.log(error);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
.then(()=> {
const rootMarkup = renderToString(
<Provider store={store}>
<RouterContext { ...renderProps } />
</Provider>
);
const initialState = store.getState();
res.status(200).send(
`
<!DOCTYPE HTML>
<head>
<script>window.__INIT_STATE__ = ${JSON.stringify(initialState)}</script>
</head>
<html>
<body>
<div id="root">${rootMarkup}</div>
</body>
</html>
`;
);
}).catch((error)=> {
console.log(error);
res.status(500).send(error);
});
} else {
res.status(404).send('Not found')
}
})