React SSR原生实现服务端渲染

代码仓:React SSR原生实现


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

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

src目录下新建三个文件夹client(客户端代码),server(服务器端代码),share(同构代码)

服务端渲染

1. 客户端渲染存在的问题

  1. 首屏等待时间长,用户体验差
  2. 页面结构为空,不利于SEO

客户端渲染需要浏览器先请求后端HTML,然后在下载JS,执行完JS并将接口请求的数据填充,最后才能将页面显示在浏览器,用户存在大量的白屏时间

服务端渲染是浏览器发送请求,服务器将HTML和数据拼接好返回给客户端,浏览器执行服务器返回的结果,此时用户可以看到静态的页面,然后在下载JS,执行完JS页面就有了动态效果了。由于服务端返回的是完整的HTML文件,所以搜索引擎能爬取到相应的内容

React SSR同构:同构指的是代码复用,即实现客户端和服务器端最大程度的代码复用;

2. webpack打包配置

Node环境不支持ESModule模块化,也不支持JSX语法,需要借助webpack打包

// webpack.server.js
const path = require('path')

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", "@babel/preset-react"]
                }
            }
        }]
    }
}

3. 客户端React附加事件

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

hydrate方法在实现渲染的时候,会复用原本已经存在的DOM节点,减少重新生成节点以及删除原本DOM节点的开销

ReactDom.hydrate(<App />, document.querySelector("#root")

优化:

  1. 合并webpack文件代码

  2. 问题:优化文件体积,bundle文件太大了,因为打包的时候会把系统模块一起打包进去,在本地运行的时候,这些模块不需要一起打包进去;

    解决:利用webpack的externals属性来解决;

    // webpack.server.js
    const nodeExternals = require('webpack-node-externals')
    const config = {
    	...,
        externals: [nodeExternals()]
    }
    
  3. 问题:渲染React组件代码时独立功能,所以把它从服务器端入口文件中进行抽离

    解决:将渲染的代码抽离出去

4. 实现服务器路由

在React SSR项目中需要实现两端路由

客户端路由是用于支持用户通过点击链接的形式跳转页面

服务器端路由是由于支持用户直接从浏览器地址栏中访问页面

客户端和服务器端公用一套路由规则

// 服务器端路由
// renderer.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import routes from '../share/routes'

export default (req) => {
    const content = renderToString(
        <StaticRouter location={req.path}>
            {renderRoutes(routes)}
        </StaticRouter>    
    )
    return `
    <html>
        <head>
            <title>React SSR</title>
            <body>
                <div id="root">${content}</div>
                <script src="bundle.js"></script>
            </body>
        </head>
    </html>
`
}

// routes.js
import Home from "./pages/Home";
import List from "./pages/List";

export default [{
    path: '/',
    component: Home,
    exact: true
}, {
    path: '/list',
    component: List
}]
// 客户端路由
// client/index.js
import React from "react";
import ReactDOM from "react-dom";
import Home from "../share/pages/Home";
import routes from '../share/routes';
import { renderRoutes } from 'react-router-config';
import { BrowserRouter } from 'react-router-dom';

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

// share/pages/Home.js
import React from "react";
import { Link } from 'react-router-dom'

function Home () {
    return <div onClick={() => console.log('hello')}>
        Home works
        <Link to="/list">jump to list</Link>
    </div>
}

export default Home

5. 实现Redux支持

在实现了React SSR的项目中需要实现两端Redux

客户端Redux就是通过客户端JS管理Store中的数据

服务器端Redux就是在服务器端搭建一套Redux代码,用于管理组件中的数据

客户端和服务器端公用一套Reducer代码

创建Store的代码由于参数传递不同所以不可以公用

注意:创建异步dispatch时报错,浏览器默认不支持异步函数,需要配置babel的polyfill支持

options: {
    presets: [["@babel/preset-env", {
        useBuiltIns: 'usage'
    }], "@babel/preset-react"]
}

服务器端store数据填充

问题:服务器端创建的store是空的,组件并不能从Store中获取到任何数据

解决:服务器端在渲染组件之前获取到组件所需要的数据

  1. 在组件中添加loadData方法,此方法有浓郁获取组件所需数据,方法被服务器端调用
  2. 将loadData方法保存在当前组件的路由信息对象中
  3. 服务器端在接收到请求后,根据请求地址匹配出要渲染的组件的路由信息
  4. 从路由信息中获取组件中的loadData方法并调用方法获取组件所需数据
  5. 当数据获取完成以后再渲染组件并将结果响应到客户端

主要思路:借助路由信息来调用dispatch获取组件所需要的数据,在响应到客户端之前先获取渲染到组件

// server/index.js
import app from './http'
import renderer from './renderer'
import createStore from './createStore'
import routes from '../share/routes'
import { matchRoutes } from 'react-router-config'

app.get('*', (req, res) => {
    const store = createStore()
    // 1.请求地址 req.path
    // 2.获取到路由配置信息 routes
    // 3.根据请求地址匹配出要渲染的组件的路由对象信息
    const promises = matchRoutes(routes, req.path).map(({route}) => {
        if (route.loadData) return route.loadData(store)
    })
    Promise.all(promises).then(() => {
        res.send(renderer(req, store))
    })
})

React警告消除

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

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

export default (req, store) => {
    // 将对象先转换为字符串
    const initialState = JSON.stringify(store.getState())
    return `
    <html>
        <head>
            <title>React SSR</title>
            <body>
                <div id="root">${content}</div>
                <script>window.INITIAL_STATE = ${initialState}</script>
                <script src="bundle.js"></script>
            </body>
        </head>
    </html>
`
}

6. 防范XSS攻击

转移状态中的恶意代码

import serialize from 'serialize-javascript'

export default (req, store) => {
    const initialState = serialize(store.getState())
    return `
    <html>
        <head>
            <title>React SSR</title>
            <body>
                <div id="root">${content}</div>
                <script>window.INITIAL_STATE = ${initialState}</script>
                <script src="bundle.js"></script>
            </body>
        </head>
    </html>
`
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Whoopsina

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值