react服务端渲染方案

1. 渲染方案介绍

1.1 什么是客户端渲染

CSR:Client Side Rendering
服务器端仅返回 JSON 数据, DATA 和 HTML 在客户端进行渲染.

1.2 什么是服务器端渲染

SSR:Server Side Rendering
服务器端返回HTML, DATA 和 HTML 在服务器端进行渲染

1.3 传统的服务端渲染(直接返回html,客户端无感知)

  1. 获取页面模板
  2. 获取数据
  3. 渲染 数据+ 模板 = 结果
  4. 把渲染结果发送给客户端

在这里插入图片描述

1.3.1 缺点
  1. 前后端代码耦合在一起,不利于开发维护
  2. 前端没有足够的发挥空间
  3. 服务端压力大
  4. 用户体验一般(如果需要查看其它页面,需要完全刷新页面)

1.4 客户端渲染

1.4.1 优点
  1. 减少服务端压力
  2. 实现前后端职责的划分(后端处理数据接口,前端实现页面渲染)
  3. 体验提升 spa 无刷新体验

在这里插入图片描述

1.4.2 不足
  • 不利于seo:

    1. seo:网站的搜索引擎的排名,搜索引擎需要知道你的网站有什么
    
    2. 搜索引擎通过程序获取你的网页内容(获取的是html字符串,不会去像spa应用一样进行渲染)
    
  • 首屏渲染慢:

    1. 传统的服务端渲染只需要请求一次,就会将结果返回给我们
    
    2. 客户端渲染需要经过多次请求才能获得所有的数据进行渲染(多次http请求)
    

1.5 SPA 应用中服务器端渲染解决的问题:ReacrSSR同构

ReacrSSR同构

同构指的是代码复用. 即实现客户端和服务器端最大程度的代码复用.

在实现过程中我们将复用代码放置在在shared文件夹(页面文件、路由文件、redux相关),

server、client分别放置服务端、客户端相关文件

2. 服务器端渲染快速开始

2.1 创建 Node 服务器

src/server/http.js

import express from "express"
const app = express()

app.use(express.static('public'))

app.listen(3001, () => {
    console.log('server is runing on http://localhost:3001')
})

export default app

2.2 实现 React SSR

src/server/index.js

  1. 引入要渲染的 React 组件
  2. 通过 renderToString 方法将 React 组件转换为 HTML 字符串
  3. 将结果HTML字符串渲染到客户端

renderToString 方法用于将 React 组件转换为 HTML 字符串, 通过 react-dom/server 导入.

import app from './http'
import React from "react"
import Home from "../shared/pages/Home"
import { renderToString } from 'react-dom/server'
import routes from "../shared/routes"
import { renderRoutes, matchRoutes } from 'react-router-config'
import { StaticRouter } from 'react-router-dom'
import { Provider } from "react-redux"
import createStore from "./createStore"

app.get('*', (req, res) => {
    const store = createStore()

    const promises = matchRoutes(routes, req.path).map(({route}) => {
        if (route.loadData) {
            return route.loadData(store)
        }
    })

    Promise.all(promises).then(() => {
        const initState = store.getState()
        const content = renderToString(
            <Provider store={store}>
                <StaticRouter location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        )
        res.send(`
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>React SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>window.INITIAL_STATE=${JSON.stringify(initState)}</script>
                    <script src="bundle.js"></script>
                </body>
                </html>
            `)
    })

})

2.3 webpack 打包配置

webpack.server.js

const path = require('path')
const nodeExternals = require('webpack-node-externals')

module.exports = {
    mode:'development',
    target:'node',
    entry:'./src/server/index.js',
    output:{
        path:path.join(__dirname, 'build'),
        filename:'bundle.js'
    },
    module:{
        rules:[{
            test:/\.js$/,
            exclude:/node_modules/,
            use:{
                loader:'babel-loader',
                options:{
                    presets:[['@babel/preset-env', {
                        useBuiltIns:"usage"
                    }], '@babel/preset-react']                
                }
            }
        }]
    },
    externals:[nodeExternals()]

}

问题: Node 环境不支持 ESModule 模块系统, 不支持 JSX 语法. 通过配置babel-loader options支持

2.4 项目启动命令配置

1. 配置服务器端打包命令: "dev:server-build": "webpack --config webpack.server.js --watch"
2. 配置服务端启动命令: "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\""

2.5 客户端 React 附加事件

  1. 实现思路分析

    在客户端对组件进行二次"渲染", 为组件元素附加事件.

  2. 客户端二次 “渲染” hydrate

    使用 hydrate 方法对组件进行渲染, 为组件元素附加事件.
    hydrate 方法在实现渲染的时候, 会复用原本已经存在的 DOM 节点, 减少重新生成节点以及删除原本 DOM 节点的开销.

通过 react-dom 导入 hydrate.

ReactDOM.hydrate( <组件/>, document.getElementById('root'))

2.6 客户端 React 打包配置

  1. webpack 配置

    打包目的: 转换JSX语法, 转换浏览器不识别的高级 JavaScript 语法
    打包目标位置: public 文件夹

const path = require('path')
module.exports = {
    mode:'development',
    entry:'./src/client/index.js',
    output:{
        path:path.join(__dirname, 'public'),
        filename:'bundle.js'
    },
    module:{
        rules:[{
            test:/\.js$/,
            exclude:/node_modules/,
            use:{
                loader:'babel-loader',
                options:{
                    presets:[['@babel/preset-env',{
            /**
            * 当我做polyfill填充的时候,去加一些低版本特性的时候,我不是把所有特性都加进来
            * 是根据你的业务代码来决定要加什么
            */
            useBuiltIns: 'usage',
            }], '@babel/preset-react']                
                }
            }
        }]
    },
}
  1. 打包启动命令配置
"dev:client-build": "webpack --config webpack.client.js --watch"

打包生成bundle.js文件

2.7 添加客户端包文件请求链接

在响应给客户端的 HTML 代码中添加 script 标签, 请求客户端 JavaScript 打包文件.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React SSR</title>
</head>
<body>
    <div id="root">${content}</div>
    <script src="bundle.js"></script>
</body>
</html>

2.8 服务器端实现静态资源访问

服务器端程序实现静态资源访问功能, 客户端 JavaScript 打包文件会被作为静态资源使用.

app.use(express.static('public'))

3. 优化

3.1 合并项目启动命令

目的: 使用一个命令启动项目, 解决多个命令启动的繁琐问题. 通过 npm-run-all 工具实现.

    "dev:server-build": "webpack --config webpack.server.js --watch",
    "dev:client-build": "webpack --config webpack.client.js --watch",
    "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"",
    "dev": "npm-run-all --serial dev:*"

3.2 服务器端打包文件体积优化

问题:在服务器端打包文件中, 包含了 Node 系统模块. 导致打包文件本身体积庞大.
解决:通过 webpack 配置剔除打包文件中的 Node 模块.

webpack,server.js


var nodeExternals = require('webpack-node-externals');

const serverConfig = merge(config, {
    ...
    externals: [nodeExternals()]

})

module.exports = serverConfig

3.3 将启动服务器代码和渲染代码进行模块化拆分

优化代码组织方式, 渲染 React 组件代码是独立功能, 所以把它从服务器端入口文件中进行抽离.

4. 路由支持

4.1 实现思路分析

  1. 在 React SSR 项目中需要实现两端路由.
  2. 客户端路由是用于支持用户通过点击链接的形式跳转页面.
  3. 服务器端路由是用于支持用户直接从浏览器地址栏中访问页面.
  4. 客户端和服务器端公用一套路由规则.

4.2 编写路由规则

shared/router.js

import Home from './pages/Home'
import List, {loadData} from './pages/List'

export default [
    {
        path:'/',
        component:Home,
        exact:true
    },
    {
        path:'/list',
        component:List,   
        loadData  
    }
]

4.3 实现服务器端路由

  1. Express 路由接收任何请求

    Express 路由接收所有 GET 请求, 服务器端 React 路由通过请求路径匹配要进行渲染的组件.

  2. 服务器端路由配置

import app from './http'
import React from 'react'
import { renderToString } from 'react-dom/server';
import Home from '../shared/pages/Home'
import routes from "../shared/routes"
import { renderRoutes, matchRoutes } from "react-router-config"
import { StaticRouter } from "react-router-dom"
import createStore from "./createStore"
import { Provider } from 'react-redux'

app.get('*', (req, res) => {
    // redux 创建仓库
    const store = createStore()
    // 匹配请求路径,执行需要在服务端执行的加载数据的函数
    const promises = matchRoutes(routes, req.path).map(({route}) => {
        if(route.loadData){route.loadData(store)}
    })
    // 数据请求结束 执行渲染
    Promise.all(promises).then(() => {
        // 匹配路由
        const content = renderToString(
            <StaticRouter location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            )
        res.send(`
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>React SSR</title>
            </head>
            <body>
                <div id="root">${content}</div>
                <script src="bundle.js"></script>
            </body>
            </html>
        `)
    })
    
})

4.4 实现客户端路由

  1. 添加客户端路由配置
import React from "react"
import ReactDOM from "react-dom"
import routes from "../shared/routes"
import { renderRoutes } from "react-router-config"
import { BrowserRouter } from "react-router-dom"
import { Provider } from 'react-redux'
import store from './createStore'


ReactDOM.hydrate(
    <BrowserRouter>
            {renderRoutes(routes)}
        </BrowserRouter>
    , document.getElementById('root'))
 

5. Redux 实现

5.1 实现思路分析

  1. 在实现了 React SSR 的项目中需要实现两端 Redux.
  2. 客户端 Redux 就是通过客户端 JavaScript 管理 Store 中的数据.
  3. 服务器端 Redux 就是在服务器端搭建一套 Redux 代码, 用于管理组件中的数据.
  4. 客户端和服务器端共用一套 Reducer 代码.
  5. 创建 Store 的代码由于参数传递不同所以不可以共用.
module:{
        rules:[{
            test:/\.js$/,
            exclude:/node_modules/,
            use:{
                loader:'babel-loader',
                options:{
                    presets:[['@babel/preset-env', {
                    // 支持dispatch的异步
                        useBuiltIns:"usage"
                    }], '@babel/preset-react']                
                }
            }
        }]
    },

5.2 实现客户端 Redux

  1. 创建 Store

src/client/createStore.js

import { createStore, applyMiddleware } from "redux"
import thunk from 'redux-thunk'
import reducer from "../shared/reducers"

const store = createStore(reducer, {}, applyMiddleware(thunk))

export default store
  1. 配置 Store

src/client/index.js

import ReactDOM from "react-dom"
import React from 'react'
import routes from "../shared/routes"
import { renderRoutes } from 'react-router-config'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from "./createStore"


ReactDOM.hydrate(
    <Provider store={store}>
        <BrowserRouter>
            {renderRoutes(routes)}
        </BrowserRouter>
    </Provider>
    , document.getElementById('root'))

 
  1. 创建 Action 和 Reducer
  2. 配置 polyfill
    由于浏览器不能识别异步函数代码, 所以需要 polyfill 进行填充.

5.3 实现服务端 Redux

  1. 创建 Store

src/server/createStore.js

import { createStore, applyMiddleware } from "redux"
import thunk from 'redux-thunk'
import reducer from "../shared/reducers"

export default () => createStore(reducer, {}, applyMiddleware(thunk))
  1. 配置 Store

src/server/index.js

...
app.get('*', (req, res) => {
    //每次创建新的store
    const store = createStore()
    const content = renderToString(
            <Provider store={store}>
                <StaticRouter location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        )
})
...

5.4 服务器端 store 数据填充

问题:服务器端创建的 store 是空的, 组件并不能从Store中获取到任何数据.
解决:服务器端在渲染组件之前获取到组件所需要的数据.

  1. 在组件中添加 loadData 方法, 此方法用于获取组件所需数据,方法被服务器端调用

    服务器端通过调用组件的 loadData 方法获取组件所需数据并将数据存储在服务器端的 Store 中
    
function loadData (store) {
    // dispatch的返回值是要触发的action对象
    // 现在通过thunk触发action返回的是异步函数
    // 异步函数返回的是promise,所以这里返回的是promise
    return store.dispatch(getList())
}
  1. 将 loadData 方法保存在当前组件的路由信息对象中.
  import Home from './pages/Home'
  import List, {loadData} from './pages/List'

  export default [
      {
          path:'/',
          component:Home,
          exact:true
      },
      {
          path:'/list',
          component:List,   
          loadData  
      }
  ]
  1. 服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
  2. 从路由信息中获取组件中的 loadData 方法并调用方法获取组件所需数据
  3. 当数据获取完成以后再渲染组件并将结果响应到客户端
import app from './http'
import React from "react"
import Home from "../shared/pages/Home"
import { renderToString } from 'react-dom/server'
import routes from "../shared/routes"
import { renderRoutes, matchRoutes } from 'react-router-config'
import { StaticRouter } from 'react-router-dom'
import { Provider } from "react-redux"
import createStore from "./createStore"

app.get('*', (req, res) => {
    const store = createStore()
    //服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
    const promises = matchRoutes(routes, req.path).map(({route}) => {
        //从路由信息中获取组件中的 loadData 方法并调用方法获取组件所需数据
        if (route.loadData) {
            return route.loadData(store)
        }
    })

    Promise.all(promises).then(() => {
        const initState = store.getState()
        const content = renderToString(
            <Provider store={store}>
                <StaticRouter location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        )
        //当数据获取完成以后再渲染组件并将结果响应到客户端
        res.send(`
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>React SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script>window.INITIAL_STATE=${JSON.stringify(initState)}</script>
                    <script src="bundle.js"></script>
                </body>
                </html>
            `)
    })

})

5.5 React 警告消除

警告原因: 客户端 Store 在初始状态下是没有数据的, 在渲染组件的时候生成的是空 ul, 但是服务器端是先获取数据再进行的组件渲染, 所以生成的是有子元素的ul, hydrate 方法在对比的时候发现两者不一致, 所以报了个警告.

解决思路: 将服务器端获取到的数据回填给客户端, 让客户端拥有初始数据.

  1. 服务器响应 Store 初始状态
import app from './http'
import React from "react"
import Home from "../shared/pages/Home"
import { renderToString } from 'react-dom/server'
import routes from "../shared/routes"
import { renderRoutes, matchRoutes } from 'react-router-config'
import { StaticRouter } from 'react-router-dom'
import { Provider } from "react-redux"
import createStore from "./createStore"

app.get('*', (req, res) => {
    const store = createStore()

    const promises = matchRoutes(routes, req.path).map(({route}) => {
        if (route.loadData) {
            return route.loadData(store)
        }
    })

    Promise.all(promises).then(() => {
        // 务器端获取到的初始数据
        const initState = store.getState()
        const content = renderToString(
            <Provider store={store}>
                <StaticRouter location={req.path}>
                    {renderRoutes(routes)}
                </StaticRouter>
            </Provider>
        )
        res.send(`
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>React SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    // 数据回填
                    <script>window.INITIAL_STATE=${JSON.stringify(initState)}</script>
                    <script src="bundle.js"></script>
                </body>
                </html>
            `)
    })

})
  1. 客户端设置 Store 初始状态

     给客户端store 设置初始数据
    
export default createStore(reducer, window.INITIAL_STATE, applyMiddleware(thunk))

6. 完整代码

代码地址:https://gitee.com/liannian9/fed-e-task-04-04/tree/master/code/2.0react-ssr

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值