react 服务端渲染

完整的项目例子:https://github.com/wolyshaw/react-init

文章内介绍服务端渲染的版本

  • react 16.2.0
  • redux 3.7.2
  • react-router-dom 4.2.2
  • react-helmet 5.2.0

公用代码部分

一般来说 Router 组件下面的都可以公用的

首先创建两个文件

  • 源文件目录下创建render.js
  • webpack.config.render.js (用于打包上面的js)

在定义路由时为了配合matchRoutes这里的路由需要是一个对象类似这样,可以创建一个单独的文件导出,方便客户端与服务端公用。

import React, { PureComponent } from 'react'
import Bundle from 'components/elements/Bundle'
const HomeLazy = require('bundle-loader?lazy&name=Home!components/pages/Home')
const NotFundLazy = require('bundle-loader?lazy&name=NotFund!components/pages/NotFund')

const Home = props => (
  <Bundle load={ HomeLazy }>
    { (Container) => <Container { ...props }/> }
  </Bundle>
)

const NotFund = props => (
  <Bundle load={ NotFundLazy }>
    { (Container) => <Container { ...props }/> }
  </Bundle>
)
export default [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '*',
    component: NotFund
  }
]

定义store, 可以创建新的文件,直接导出一个创建store方法方便客户端与服务端公用

import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'
import store from '../reducers'

const middleware = [ thunk ]

export const appStore = init => createStore(
  store,
  init,
  applyMiddleware(...middleware)
)

定义好路由和store之后就可以处理请求了

在render.js内引入需要的依赖

import fs from 'fs' // 读取文件,可以用来读取html模板文件
import path from 'path'
import React from 'react'
import express from 'express'
import { Helmet } from 'react-helmet' // 用于获取标题 meta信息
import { Provider } from 'react-redux'
import { StaticRouter } from 'react-router'
import { renderToString } from 'react-dom/server'
import config from '../config'
import routes from 'util/routes' // 定义路由
import { appStore } from 'util/store' // redux store
import Application from './components' // 应用的根组件
import { matchRoutes, renderRoutes } from 'react-router-config' // 获取当前路由下的组件和渲染组件的方法
// 获取模板
const tpl = fs.readFileSync(
  path.join('dist', 'index.html')
).toString()

// 内容替换
const renderFullPage = (html, meta, initialState) => {
  return tpl
    .replace('<meta data-meta="meta">', meta)
    .replace('<div id="app"><div class="loadingContainer"><div><div class="loadingWhirlpool"></div></div></div></div>', `<div id="app">${html}</div>`)
    .replace('<script>window.__INITIAL_STATE__={}</script>', `<script>window.__INITIAL_STATE__=${JSON.stringify(initialState)}</script>`)
}
app.get('*', (req, res) => {
  // 处理所有的get请求
  const store = appStore({}) //创建一个store
  const branch = matchRoutes(routes, req.url) // 获取当前路由下面的组件
  let fetchs = []
  branch.map(({route, match}) => route.component.Fetchs && typeof route.component.Fetchs === 'function' ? route.component.Fetchs(match.params) : [])
    .map(s => typeof s === 'function' ? fetchs.push(store.dispatch(s)) : s.map(_s => fetchs.push(store.dispatch(_s))))

  // route.component 这个就是当前路由的组件了,Fetchs是预先定义的静态方法会给出需要的action,然后dispatch,这里的action必须返回Promise,因为可能有网络请求,异步的情况

  Promise.all(fetchs).then(() => {
    // 组件下的所有action都dispatch,并且成功后,开始获取html
    let context = {} // 定义一个对象,经过处理会返回组路由的信息,比如302跳转这些
    let html = renderToString(
      <Provider store={ store }>
        <StaticRouter
          location={ req.url }
          context={ context }
        >
          <Application/>
        </StaticRouter>
      </Provider>
    )

    const helmet = Helmet.renderStatic()
    // 获取标题meta等信息: [详细信息](https://www.npmjs.com/package/react-helmet#server-usage)
    if (context.url) {
      res.writeHead(302, { Location: context.url })
      res.end()
    } else if(context.status === 404) {
      res.status(404)
      res.send(renderFullPage(html, ([helmet.title.toString(), helmet.meta.toString()].join('')), store.getState()))
    } else {
      res.send(renderFullPage(html, ([helmet.title.toString(), helmet.meta.toString()].join('')), store.getState()))
    }
  })
})

到此render.js基本就已经写完了

webpack.config.render.js

这个配置文件和普通的webpack配置文件一样主要是加一个target: 'node'

然后就是使用webpack将这个js打包一下就可以执行了

到此就可以运行了,如果需要运用到生产环境还需要进一步优化做一个错误的处理,后面再说。。。