React 服务端渲染

2016年7月21日

本文写于 2016 年,示例基于 gulp、babel-preset-es2015、react-router v3 与 class/decorator 写法;今日更常见的是 Next.js / Remix 等框架内置的 SSR/RSC。

得益于 Virtual DOM 和 JSX,React 并不需要依赖于 DOM,所以能在服务器上渲染 React 应用,并且向客户端发送 HTML 代码。

Babel es6+

Node 目前只支持部分 es6 特性,我们需要 Babel 把代码转换一次,新建 .babelrc 文件

1234{  "presets": ["es2015", "react", "stage-0"],  "plugins": ["babel-plugin-transform-decorators-legacy"]}

为了自动转换代码,我们使用 gulp

1234567891011121314151617181920const 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:

123456789101112131415161718192021222324export 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:

12345678910import 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 中直接处理:

123456789101112131415161718import { 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 钩子:

1234567891011121314151617181920212223242526import 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 后才会向客户端发送数据

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import { 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>              <html>                <head>                  <script>window.__INIT_STATE__ = ${JSON.stringify(initialState)}</script>                </head>                <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')    }})

评论

0 条讨论

匿名评论需填写昵称、邮箱,并通过人机验证。邮箱不会公开展示,提交后需审核通过才会显示。

还没有评论,来抢沙发吧。