服务端渲染(Universal/Isomorphic)之React (上)

阅读9216评论53

这个世界真奇妙, 以前只有服务端渲染, 甚至html代码和后端语言代码都混编在一起, 后来Ajax无刷新加载流行一时, 慢慢有了前端框架, 使用前端路由, 干脆全部都是Ajax, 哈哈. 但是这SEO的问题却是个大问题, 同时使用服务端渲染可以加快首屏网页打开速度.

但总不能后端写一套代码, 前端也弄一套吧, 再个这路由也要一样啊, 所以必须还要共用一套代码. 当然这一切React都帮我们解决了. react 提供了 renderToString 方法可以将组件代码在后端渲染成Html代码发送给前端, 同时react-router 也提供了 match 方法在后端来匹配路由. 那还有个问题,如何初始化数据呢? 后端初始化了数据发送给前端, 前端共享这个数据, 不用再次发送请求. 还好有redux这东东, 我们只需要共享维护这一套状态状态树就OK了.
但是在实践的过程中也遇到一些问题, 顺便记录下来.

一, 初始化数据

在前端渲染的情况下启动时会有大量的ajax请求初始化数据, 那如何方便的将这些需要初始化请求的一次性在后端执行. 根据Facebook前端工程师 Stepan 在第二届前端开发者大会的分享, 我们在每个容器组件中添加一个静态方法:

import * as Actions from '../actions'
static fetchData(params){
   return [Actions.getUserInfo(),Actions.getIndexImage()]
}

这个方法返回一个数组, 里面是需要初始化请求的action方法. 也可以传入一些参数, 提供给action方法. 这里使用了redux,通过promise中间件, 让这些action都返回一个promise

export default function promiseMiddleware() {
  return next => action => {
    const { promise, type, ...rest } = action

    if (!promise) return next(action)
    return promise
      .then(response => ({json: response.data, status: response.statusText}))
      .then(({json,status}) => {
        next({ ...rest, json, type: SUCCESS })
        return true
      })
      .catch(error => {
        next({ ...rest, error, type: FAILURE })
        return false
      })
  }
}

那么在后端如何去处理呢.

async function fetchAllData(dispatch, components, params) {
  const needs = components
      .filter(x=>x.fetchData)
      .reduce((prev,current)=>{
        return current.fetchData(params).concat(prev)
      },[])
      .map(x=>{
        return dispatch(x)
      })
  return await Promise.all(needs)
}

在后端我们通过一个fetchAllData函数, 将每个组件的的fetchData方法返回的action都提出来, 并全部dispatch, 同时这里使用async/await ES7异步处理语法, 就可以得到需要初始化的全部数据了. 通过window.INITIAL_STATE 传给前端.

二, cookie 处理

但有些数据,可以是需要传cookie到服务器才能获取的, 比如通过是否存在token来判断是不是登录用户, 用户权限不一样,获取的数据不一样. 所以这部分要先获取cookie, 这里用到了react-cookie.

import reactCookie from 'react-cookie'
  reactCookie.plugToRequest(req, res)
  const history = createMemoryHistory()
  const token = reactCookie.load('token') || null
  const store = configureStore({auth:fromJS({
    token: token,
    user: null
  })}, history)

三, redux-devtools 开发工具的处理

如果是用chrome 扩展是没有问题的, 但是如果嵌入网页的话, 由于它只出现在开发环境的客户端, 并且不能放入前后共用的代码, 否则会出现前后渲染不一致的错误. 所以这里单独通过render方法追加到body最后.

import React from 'react'
import { render } from 'react-dom'
import DevTools from './components/DevTools'
export default function createDevTools(store) {
  if(__DEVCLIENT__ && __DEVTOOLS__ && !window.devToolsExtension){
    setTimeout(() => render(
      <DevTools store={store} />,
      window.document.body.appendChild(document.createElement('div'))
    ), 10)
  }
}

四, 生产环境处理HTML

得到初始化数据和renderToString转换的字符串, 要将这些加进html代码中, 通常用如下一个方法, 通过模板字符串替换变量, 如下:

function renderFullPage(renderedContent, initialState) {
  return `<!doctype html>
  <html>
    <head>
      <base href="/">
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width">
      <title>Jack Hu's blog for React</title>
      <meta name="description" content="This is Jack Hu's blog. use react redux.">
      <meta name="keyword" content="Jackblog react redux react-router react-redux-router react-bootstrap react-alert">
      <link rel="stylesheet" href="/style.css"/>
    </head>
    <body class="day-mode">
    <div class="top-box" id="root">${renderedContent}</div>
    <script>
      window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
    </script>
    <script type="text/javascript" charset="utf-8" src="/bundle.js"></script>
    </body>
  </html>
  `
}

但是这种情况, css文件和js文件都是写死的, 在开发情况无所谓, 但是生产环境有时会给文件名加hash字符串. 所以这里通过webpack插件 html-webpack-plugin 生成 ejs模板来提供给server渲染.

      new HtmlWebpackPlugin({
        favicon:path.join(__dirname,'../src/favicon.ico'),
        title: "Jackblog react redux版",
        template: path.join(__dirname,'../src/index.html'),
        filename: 'index.ejs',
        inject:'body',
        htmlContent:'<%- __html__ %>',
        initialData:'window.__INITIAL_STATE__ = <%- __state__ %>',
        hash:false,    //为静态资源生成hash值
        minify:{    //压缩HTML文件
          removeComments:false,    //移除HTML中的注释
          collapseWhitespace:false    //删除空白符与换行符
        }
      }),

服务端使用express, ejs模板引擎来渲染.

return res.render('index', {__html__: componentHTML,__state__: JSON.stringify(initialState)})

先记录来这吧. 以后有更好的解决办法再写下篇.

具体实现请参考: https://github.com/jackhutu/jackblog-react-redux 更新到了react v15.0.1

51条评论添加新评论
gdhzkkgdhzkk2016.5.8 4:17

终于登录成功了

gdhzkkgdhzkk2016.5.8 4:18

为什么评论发表后,,发表的文字还在评论框里

winky@gdhzkk 哈哈, 别太在意细节.

NikolasNull@gdhzkk 787

Joe。Joe。2016.5.10 13:9

1

懵懂#一刻懵懂#一刻2016.5.11 6:10

qqqq

tobyforevertobyforever2016.5.12 7:16

博主您好,看了你的博客觉得挺好的,请问能讲一下怎么学习前端的这些东西吗,感觉现在前端新东西好多,js都变了好多,还有flux react vue之类的东西,感觉有点晕

Jack@tobyforever 晕就对了, 先从简单的玩起, react不一定要用flux, 或者先不玩框架, 玩玩jquery, 慢慢来.

Go7hicGo7hic2016.5.13 3:22

凤飞飞

Jack@Go7hic I Love 凤飞飞

near45near452016.5.15 6:50

不错 上来测试一下

鱼和熊掌@near45 +1

蛐蛐蛐蛐2016.5.15 13:41

Good job.

Long Time *@蛐蛐 Good job.

Long Time *Long Time *2016.5.17 9:34

Good job.

Long Time *@Long Time * Good job.

一直很安静@Long Time * 111

DearChenDearChen2016.5.18 9:21

Hello World

毛绒绒的大腿毛绒绒的大腿2016.6.22 10:26

test

NikolasNull@毛绒绒的大腿 3535

NikolasNull@NikolasNull 42342

浩子浩子2016.6.24 5:51

test1

NikolasNull@浩子 434

NikolasNull@NikolasNull 4234

NikolasNull@NikolasNull 32131

NikolasNull@NikolasNull

true blue1true blue12016.6.27 3:48

恩 !

NikolasNull@true blue1 3211

true blue1true blue12016.6.27 3:49

1

true blue1true blue12016.6.27 3:53

dsa

true blue1true blue12016.6.27 3:56

2

苏格拉苏格拉2016.6.28 6:59

test

苏格拉苏格拉2016.6.29 8:0

xiexie

Jory ZhouJory Zhou2016.7.6 8:53

不错

guoyijguoyij2016.7.15 7:24

hiddaorearhiddaorear2016.7.18 6:43

test

夜神月啊啊夜神月啊啊2016.7.25 8:21

啊啊啊

on my way@夜神月啊啊 很棒啊

JerryShi@on my way 我也觉得很棒。

士且土申士且土申2016.8.3 5:40

hello world

EcmaProSrc.P/kaEcmaProSrc.P/ka2016.8.4 11:39

hello

EcmaProSrc.P/kaEcmaProSrc.P/ka2016.8.4 11:39

hello

EcmaProSrc.P/kaEcmaProSrc.P/ka2016.8.4 11:39

hello

EcmaProSrc.P/kaEcmaProSrc.P/ka2016.8.4 11:39

&lt;script&gt;alert(1);&lt;/script&gt;

EcmaProSrc.P/ka@EcmaProSrc.P/ka 3424

KolfKolf2016.8.7 10:18

真可以登陆?

bobshi2005@Kolf

smallossmallos2016.8.12 2:50

小葱Heart小葱Heart2016.8.19 1:39

请问 package.json 中 betterScripts 字段 是什么作用 有没有文档连接

Jack@小葱Heart https://github.com/benoror/better-npm-run

hongyangzhaohongyangzhao2016.9.1 2:12

有个bug,在首页跳转本页时,只是client端路由变化,数据加载是通过componentDidMount里调用,而不是服务端渲染出来的,但是在本页刷新时,服务端却match了路由的变化,页面是由服务端渲染出来的。请问,这是什么问题呢?

hongyangzhao@hongyangzhao 貌似就应该这样的

hcyhehehcyhehe2016.10.7 2:48

博主,你的登录密码加密方式是什么,最近在github看你的源码,有点不懂。。本人小白一枚,求解答。。

JackHu@hcyhehe crypto.pbkdf2Sync(password, salt, iterations, keylen, digest), node.js的crypto库, 可查看官方文档.

xwlyyxwlyy2016.10.7 9:40

大神,我有个问题,虽然我已经有了一番猜测,但不是特别确定,所以想请教下印证我的猜想。 你是在前后端项目中都设置cookiedomain为.jackhu.top来解决跨域cookie共享的问题吧。这么做的前提是前后端都部署在.jackhu.top域名下。现在我自己部署了一个demo,前端部署在github pages上,域名是jackblog.paidepaiper.top,后端部署在heroku上,用的是heroku给的域名jackblog-express.herokuapp.com。因为前后端主域名不一样,所以我不能设置后端的cookiedomain为.paidepaiper.top,只能设置为空。这样前端获取到的后端cookie就在jackblog-express.herokuapp.com域名下。这样做对local登录没有影响,因为local登录后由后端返回token,前端获取token后设置cookie到自己的域名下。但第三方登录就有问题了,第三方登录认证通过后由后端设置cookie,这样token就会在jackblog-express.herokuapp.com域名下。前端无法获取到jackblog-express.herokuapp.com域名下的token,所以一直显示用户未登录。 你的代码没问题,是我自己在部署的时候碰到的一个奇葩问题,上面这些是我自己分析的,不知道对不对。

Jack@xwlyy 你说的没错,我设cookiedomain是为了让四个版本共享cookie, 你可以不用设置, 可以去掉这个配置.

szy0syzszy0syz2016.10.17 2:8

写得很好~

KolfKolf2016.10.17 6:18

test

sinian1989sinian19892016.12.8 7:29

博主,请教个问题,最好能加下我QQ282798275,或者告诉我你的, 我现在是页面里有很多地方有window对象或者是有window.navigator.userAgent;这样node渲染时运行server.js的时候,serverRender报错,没有window对象,想问下这种怎么解决呢

sinian1989麻烦看到请快点回复下 谢谢了

xiashengxiasheng2016.12.26 5:24

12112

cqbus405cqbus4052017.1.8 14:5

cool

cqbus405cqbus4052017.1.8 14:5

mj

tianzhihentianzhihen2017.3.21 13:1

楼主已超神!能否带大家一起学习?创建一个QQ群吧

tianzhihentianzhihen2017.3.21 13:1

test

tiger6tiger62017.8.4 3:18

12423

maoyuyangmaoyuyang2017.9.4 14:43

sad

maoyuyangmaoyuyang2017.9.4 14:43

sdas

java_luojava_luo2017.11.16 3:28

大牛啊,我还是云里雾里,不知道服务端渲染要怎么搞

huanglianghuangliang2017.12.13 12:12

ButonixButonix2018.1.25 19:1

dfhdfhdfhd

BrandBrand2018.1.31 5:18

厉害厉害

BrandBrand2018.1.31 5:19

正在学习react

luluhan1234luluhan12342018.4.25 1:36

我想试试评论,我觉得前面发乱七八糟的也是这么想的

luluhan123@luluhan123 我也回复试试看

NikolasNullNikolasNull2018.6.2 13:44

uiui