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打包一下就可以执行了
到此就可以运行了,如果需要运用到生产环境还需要进一步优化做一个错误的处理,后面再说。。。