菜鸟朱茱霞的前端搬砖史

复盘---大白话解释react-ssr

2019-01-23  本文已影响0人  朱珠霞

最近刚学了一些react-ssr的内容,刚好用这个来码了自己的个人简历网站。现在临近收尾工作,写一篇笔记来记录复盘一下。

源码查看

如果希望可以无痛理解,可能需要提前理解react相关技术栈,如react-redux、react-router等

react-redux思维导图

众所周知,一般的SPA会因为客户端需要先运行一遍渲染出DOM,因此一般会出现首屏加载白屏的现象,另外也会存在SEO较差的问题。SSR的出现就是为了解决这两个问题。(emm..我用这个码了一个个人简历网站,也是为了提高SEO...哈哈哈)

什么是同构

同一套react代码在服务端、客户端都运行一遍。当客户访问网页,react代码先在服务端运行一遍,返回一个html的字符串模板到客户端。客户端接收到html & JavaScript脚本。JavaScript脚本的react代码接管服务端返回的html模板。

//app.js
import React from 'react'
export default ()=>(
    <div>this is app</div>
)

// client-side
import ReactDOM from 'react-dom'
import App from './app.js'

ReactDOM.hydrate(App,document.getElementById('root'))

//server-side
import express from 'express'
import { renderToString } from 'react-dom/server'
import App from './app.js'

const app = express()
const html =(content)=>(`
    ...
<body><div id="root"> ${content} </div></body>
    ...
`)
app.get('*',(req,res)=>{
    const content = renderToString(App)
  res.send(html(content))
})

一个简单的客户端/服务端同构就实现了。react-dom中有个API renderToString可以将react生成的虚拟DOM转化为字符串。

简单知识点:

  1. server-side使用renderToString生成字符串模板后,返回给客户端。
  2. client-side使用hydrate接管server端返回的html模板,继续运行react代码。

hydrate相比于普通的render,免去了创建dom节点的工作,但仍然需要完成dom diff,和dom patch的工作,为相应的DOM节点绑定事件,运行react生命周期中componentDidMount后的工作。

这里需要注意的是:当客户端渲染的结果与服务端渲染的结果不一致的时候,将会出现error xxx does not match xxx

这时候就要回顾一下自己的代码哪里出错了。

但在实际项目中,react 同构我们需要考虑的问题还有很多:

  1. 服务端和客户端运行环境不一样,服务端运行在node环境下,客户端运行在浏览器下,因此服务端没有window这个全局对象。
  2. 服务端遵循CommonJS标准,客户端支持ES6模块。
  3. 客户端/服务端的数据如何统一,如果有些数据需要异步(请求别的服务端)来获取,如何控制?
  4. html模板渲染出来了,但没有提前准备CSS资源,用户体验也不好。
  5. 应用的路由如何控制,在服务端/客户端如何设置

带着这些问题,我们来搭建一个简单的react-SSR环境

react-SSR环境搭建

在开始之前,我先说一下这次项目我所使用的技术栈以及目录结构:

//目录结构
.
├─ build/                    # webpack配置目录
├─ dist/                     # server-side 编译后的代码
├─ public/                   # client-side 编译后的代码(静态资源)
├─ src/                      # 源码
│   ├─── App/                # React代码
│   │     ├─── components/   # 页面中的组件
│   │     ├─── pages/        # 页面组件
│   │     ├─── store/        # 状态管理 redux
│   │     ├─── styled/       # 所有styled-component
│   │     └─── index.jsx     # App
│   ├─── config/             # APP的一些配置文件,如Router、leancloud配置等
│   ├─── client/             # 客户端渲染代码
│   └─── server/             # 服务端代码
│         ├─── proxy.js      # 路由代理
│         ├─── render.js     # 负责生成html模板
│         └─── index.jsx     # node-server 入口
├─── .gitignore
└─── package.json

1. 搭建ssr环境第一步

使用webpack分别给客户端和服务端写一份打包配置

# webpack.client.js
const path = require('path')
const Merge = require('webpack-merge')
const baseConfig = require('./webpack.base')

const config = {
  entry: path.resolve(__dirname,'../src/client'),
  output:{
    filename:'index.js',
    path: path.resolve(__dirname,'../public')
  }
}
module.exports = Merge(config,baseConfig)

# webpack.server.js
const path = require('path')
const Merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base')

const config = {
  target: 'node',
  entry: path.resolve(__dirname, '../src/server'),
  output: {
    filename: 'server.js',
    path: path.resolve(__dirname, '../dist'),
    libraryTarget: 'commonjs2'
  },
  externals:[nodeExternals()]
}
modules.exports = Merge(config,baseConfig)

# webpack.base.js
const path = require('path')
module.exports = {
  mode: process.env.NODE_ENV ||'production',
  resolve: {
    extensions: [ '.js', '.jsx' ]
  },
  module:{
    rules:[{
      test: /\.(js|jsx)?$/,
      loader:'babel-loader',
      exclude:[
        path.resolve(__dirname, '../node_modules')
      ],
      options:{
        presets:['react','stage-0',['env',{
          target:{
            browser:['last 2 versions']
          }
        }]
        ]
      }
    }
}

# npm script
"build:server":"webpack --config ./build/webpack.server.js --watch" 
"build:client":"webpack --config ./build/webpack.client.js --watch" 
"dev:start": "node ./dist/server.js"

第二步:配置css文件

这次我使用的是styled-component

# webpack.base.js
// babel-loader options
plugins:['babel-plugin-styled-components']
//server-side
import { ServerStyleSheet } from 'styled-components'

 const sheet = new ServerStyleSheet()
 const content = renderToString(sheet.collectStyles(App)) //App---root component
 const style = sheet.getStyleTags() //获取css样式,插入在html模板中
# html
`
<html lang="zh-Hans">
<head>
    ${ style }
</head>
<body>
  <div id="root">${content}</div>
  <script src="./index.js"></script>
</body>
</html>
`

第三步:App的数据管理

# store
import {createStore,compose,applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
import {fromJS} from 'immutable'

let enhances,defaultState

if(typeof window === 'object'){ //client-side
  const componentEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
  enhances = componentEnhancers(applyMiddleware(thunk))
  defaultState = fromJS(window.INITIAL_STATE)  // 注水第2步(第1步看最下面)
} else { //server-side
  enhances = compose(applyMiddleware(thunk))
}
// 脱水
const getStore = defaultState ? 
  createStore(reducer,defaultState,enhances) :
  createStore(reducer,enhances) 

export default getStore

# server-side
import getStore from './store'
const store = getStore()
const initState = store.getState() 
# html
//注水第1步,将服务端的store数据传到window全局对象的__INITIAL_STATE__中
`<script>window.__INITIAL_STATE__ = ${JSON.stringify(initState)}</script> `

第四步:路由设置

关于react-router,跟平常的react App差不多,但是在服务端里,由于没有DOM,因此我们不能用<BrowserRouter>来包裹routes,react-router-dom 提供了<staticRouter>用于node端。

# Router config
import App from 'app.jsx'
import IndexPage from 'indexPage.jsx'
import LoginPage from 'loginPage.jsx'

export default [{
  path: '/',
  component: App,
  routes:[{
    path: '/',
    exact: true,
    component:IndexPage,
    loadData: IndexPage.loadData
  },{
    path: '/login',
    component: LoginPage
  }]
}]

# server-side
import Router from './Router.js'
import { renderRouter } from 'react-router-config'
import { staticRouter } from 'react-router-dom '
app.get('x',(req,res)=>{
  const App= (
    <StaticRouter location ={req.path} context={context}>
        { renderRoutes(Router) }
    </StaticRouter>)
const content = renderTostring(App)
})

# client-side
const App =(
     <BrowserRouter>
      { renderRoutes(Router) }
    </BrowserRouter>
)

ReactDOM.hydrate(App,document.getElementById('root'))
# server-side
import { matchRoutes } from 'react-router-config'
import getStore from './store'

// ...
const store = getStore()
const currentRoutes = matchRoutes(Router, req.path)
currentRoutes .map( matchItem=>{
    if(!!matchItem.route.loadData){
      const newPromise = new Promise((resolve, reject)=>{
        matchItem.route.loadData(store).then(resolve).catch(reject) //这里将服务端的store传给App
      })
      promises.push(newPromise)
    }
  })
  Promise.all(promises).then(()=>{
    const html = render()
    res.send(html)
  }).catch((err)=>{
    res.status(500).end('sorry request error')
  })

# app component
IndexPage.loadData =({dispatch})=>{
  return dispatch(action.getHomeList())
}

# action.js
// thunk中间件的 函式 action
const  url={
   // ... other url
   homelist:'/homelist'
 }
export const getHomeList =()=>((dispatch,getState,axiosInstance)=>{
  return axiosInstance.get(url.homelist).then( response=>{
    const action = {
      type:GETHOMELIST,
      list:response.data.list
    }
    dispatch(action)
  })
})
  1. 实现404页面
    首先在路由配置Router.js 中设置路由:当访问路径不满足指定路径时,跳转到NotFound组件
export default [{
  path: '/',
  component: App,
  routes:[{
    path: '/',
    exact: true,
    component:IndexPage,
    loadData: IndexPage.loadData
  },{
    path: '/login',
    component: LoginPage
  }]
},{
  component:NotFound 
}]

NotFound组件里,对context进行修改:

# NotFound component
componentwillMount(){
  this.props.staticContext && (this.props.staticContext.NotFound = true)
}

在服务端对context进行判断
context.NotFound ? res.status(404).send(html) :res.send(html)

  1. 实现301重定向
    当组件里有Redirect组件的时候,客户端进行重定向,同时也会向服务端的stacticContext发送一段信息:
{
  action:' REPLACE ',
  location: xxx,//
  url: //重定向的路由
}

可以借助这个特性实现重定向。

总结

以上是关于react-ssr的一些概念。我这个项目的架构相对来说还是比较简单,对于第一次接触react-ssr的人,可能会比较好理解,有兴趣可以clone下来学习一下。
对于这个项目架构的设计其实对我来说并不完美。如果说打分勉强算 5、6分吧。它还有很多需要优化的地方:比如没有eslint的检查配置,比如开发体验还不是最优的,每次更新代码不能自动刷新页面更新...等等
anyway, learn by doing. 大家渣油啊。

上一篇 下一篇

猜你喜欢

热点阅读