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')
        }
})

发表评论

加载中...