React SSR - 01 SSR 介绍 和 快速开始

React SSR 介绍

什么是客户端渲染

CSR:Client Side Rendering

数据和 HTML 的拼接是在客户端(浏览器)使用 JavaScript 完成的,服务端只需要返回 JSON 数据即可。

使用 React 开发的项目是客户端渲染的单页应用(SPA)。

什么是服务器端渲染

SSR:Server Side Rendering

数据和 HTML 的拼接是在服务器端完成的,客户端向服务器端发送请求,服务器端返回拼接好的 HTML,客户端只需将其显示出来。

客户端渲染存在的问题

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

CSR 过程

在这里插入图片描述

  1. 用户首次访问页面,客户端向服务器发送请求,服务端返回 HTML,但是 HTML 文档只包含一些 CSS 资源链接和 JS 资源链接,等待期间页面没有展示的内容。
  2. 浏览器识别 HTML 文档,向服务器请求资源文件,请求过程中页面上没有展示内容。
  3. 加载完资源文件,就要执行对应的 JS 脚本,在执行 JS 脚本的时候又要向服务器获取当前页面所需要的数据,请求数据的过程中,页面上依然没有展示内容(或者渲染了一些静态内容,但是展示动态数据的地方还是空的)。
  4. 当数据请求完,在客户端又要使用 JavaScript 把请求的 JSON 数据和 HTML 拼接好,将拼接好的结果显示在页面中。到此页面才算加载完成。

在整个过程中,页面是没有内容或内容不完整的。即首屏等待时间长,用户体验差。

搜索引擎爬虫通常只会解析该请求返回的 HTML 内容,除非是针对性的爬虫,否则会忽略其它资源和请求数据的请求。

而用户请求客户端渲染的页面获取的 HTML 文档,页面结构是空的(浏览器查看页面源代码),所以爬虫抓取不到什么内容,也就不利于 SEO。

SSR 过程

在这里插入图片描述

  1. 用户首次访问页面,客户端向服务器端发送请求,服务器端将数据和 HTML 拼接好的结果返回给客户端。此时浏览器还是处于等待状态。
  2. 浏览器执行服务器端返回的 HTML 内容,它是包含了服务器端数据的,所以用户可以看到页面内容,只不过现在页面上展示的只有静态的 HTML 内容,没有动效果,浏览器还是要向服务端发送请求,获取资源文件。
  3. 当请求完 JS 文件,浏览器就会执行 JS 脚本,执行完成后页面就会有动态效果。

整个过程,用户早早的就能看到页面内容,只不过没有动态效果,要等待JS文件请求并执行完成后才会有动态效果。

用户可以更快看到页面内容,解决了首屏加载慢的问题。

而服务器页面请求返回的是完整的 HTML 内容,搜索引擎的爬虫工具能抓取相关内容,从而解决了 SEO 的问题。

同构

同构指的是代码复用。

单纯的 SSR 只是展示静态内容的传统技术,我们仍需要 SPA 交互体验。

最好的方案就是 SSR + SPA 相结合,即在实现服务端渲染的时候,还要实现客户端渲染,首次访问页面是服务端渲染,基于首次访问的后续的交互就是 SPA 的效果,这样就保留了两个技术的优点。

两种技术有大量可重用的代码,客户端路由、服务器端路由、客户端 Redux、服务器端 Redux 等,最大程度的复用这些代码,就是同构

现在所说的服务端渲染基本上都是 SSR + SPA 的同构渲染,不是传统上的服务端渲染。

服务端渲染快速开始

项目结构

创建 react-ssr 文件夹,创建项目结构:

└─ src					# 源代码文件夹
    ├─ client			# 客户端代码
    ├─ server			# 服务器端代码
    └─ share			# 同构代码

安装依赖

# 自动重新执行node命令的工具
nodemon
# babel
@babel/cli
@babel/core
@babel/preset-env
@babel/preset-react
babel-loader
# Node 服务器
express
# react
react
react-dom
# webpack
webpack
webpack-cli

创建 Node 服务器

// src/server/http.js
import express from 'express'

const app = express()

app.listen(3000, () => console.log('app is running on 3000 port'))

export default app

// src/server/index.js
import app from './http'

app.get('/', (req, res) => {
  // 返回响应内容
})

React SSR 实现

实现步骤:

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

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

对于要渲染的 React 组件,属于客户端和服务端同构代码,所以放到 share 文件夹下。

// src\share\pages\Home.js
import React from 'react'

function Home() {
  return <div>Home works</div>
}

export default Home

// src/server/index.js
import app from './http'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from '../share/pages/Home'

app.get('/', (req, res) => {
  const content = renderToString(<Home />)
  res.send(`
    <html>
      <head>
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${content}</div>
      </body>
    </html>
  `)
})

服务端程序 webpack 打包配置

Node 环境不支持 ESModule 模块系统,不支持 JSX 语法。

所以需要通过 webpack 调用 Babel 对服务端代码进行打包,然后运行打包后的代码启动项目。

// 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']
          }
        }
      }
    ]
  }
}

配置打包命令:

// package.json
"scripts": {
  // 服务器端打包命令(监听文件变化)
  "dev:server-build": "webpack --config webpack.server.js --watch",
  // 服务器端启动命令(监听打包目录文件变化)
  "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\""
},

执行打包命令:

npm run dev:server-build

新开命令行窗口运行打包结果:

npm run dev:server-run

访问http://localhost:3000/,查看页面源代码包含了页面展示的内容。

现在就完成了一个简单的服务端渲染。

hydrate 客户端二次渲染

为什么要二次渲染

在 React 组件中为元素添加事件,经过服务端渲染后并没有被保留下来:

// src\share\pages\Home.js
import React from 'react'

function Home() {
  return <div onClick={() => {console.log('click')}}>Home works</div>
}

export default Home

渲染结果:

<div id="aa" data-reactroot="">Home works</div>

这是因为 renderToString 渲染的 HTML 是纯静态 HTML,没有任何交互效果。

解决办法:在客户端二次“渲染”。

react-dom 提供的 hydrate 方法类似 render 方法,用于二次渲染。

它在渲染的时候会复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销,只进行事件处理绑定。

hydraterender 的区别就是 hydrate 会复用已有节点,render 会重新渲染全部节点。

所以hydrate 主要用于二次渲染服务端渲染的节点,提高首次加载体验。

二次渲染组件

// src\client\index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Home from '../share/pages/Home'

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

这个文件将作为静态资源文件被客户端请求。

但是当前代码无法在浏览器环境直接运行,需要 webpack 打包编译。

客户端 webpack 打包配置

webpack 打包配置:

  • 打包目的:转换 JSX 语法,转换浏览器不识别的高级 JS 语法。
  • 打包目标位置:public 文件夹(存放客户端请求的静态资源文件)。
// webpack.client.js
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', '@babel/preset-react']
          }
        }
      }
    ]
  }
}

配置打包命令:

// package.json
"scripts": {
  // 客户端打包
  "dev:client-build": "webpack --config webpack.client.js --watch",
  "dev:server-build": "webpack --config webpack.server.js --watch",
  "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\""
},

新开命令行窗口执行打包命令:

npm run dev:client-build

引入静态资源文件

客户端的打包的 JS 文件会被作为静态资源引入,打包文件存放在 public 文件夹,服务器端需要为这个目录配置静态资源访问功能,这样从根目录访问的静态资源文件会去 public 文件夹下寻找。

// src/server/http.js
import express from 'express'

const app = express()

// 配置静态资源访问
app.use(express.static('public'))

app.listen(3000, () => console.log('app is running on 3000 port'))

export default app

在服务端返回的 HTML 中添加引入静态资源文件的 script 标签:

// src/server/index.js
import app from './http'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from '../share/pages/Home'

app.get('/', (req, res) => {
  const content = renderToString(<Home />)
  res.send(`
    <html>
      <head>
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="bundle.js"></script>
      </body>
    </html>
  `)
})

现在点击事件就可以生效了。

优化

合并 webpack 配置

webpack.server.jswebpack.client.js 配置文件中存在一些相同的配置,可以将重复配置抽象到一个配置文件中。

安装 webpack 配置合并工具:

npm i webpack-merge

公共配置文件:

// webpack.base.js
module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  }
}

修改配置文件:

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

const config = {
  entry: './src/client/index.js',
  output: {
    path: path.join(__dirname, 'public'),
    filename: 'bundle.js'
  }
}

module.exports = merge(baseConfig, config)

// webpack.server.js
const path = require('path')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')

const config = {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  }
}

module.exports = merge(baseConfig, config)

接着重新运行打包命令即可。

合并项目启动命令

现在启动项目需要运行三个启动命令。

通过 npm-run-all 工具可以合并多个命令的执行,解决多个命令启动的繁琐问题。

安装:

npm i npm-run-all

添加脚本:

"scripts": {
  "dev:client-build": "webpack --config webpack.client.js --watch",
  "dev:server-build": "webpack --config webpack.server.js --watch",
  "dev:server-run": "nodemon --watch build --exec \"node build/bundle.js\"",
  // 并行执行 `dev:` 开头的命令
  "dev": "npm-run-all --parallel dev:*"
},

打断所有命令,重新执行 npm run dev

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

使用 webpack 打包服务端文件时,通常不希望打包 node_modules 下的依赖模块。

因为在服务端运行后端代码时应该已经安装了依赖,应用程序应该从 node_modules 目录下引入模块,而不是全部合并到打包文件增加打包文件大小。

解决:通过 webpack-node-externals 配置剔除打包文件中的依赖模块。

当前服务端打包文件大小为 1MB 左右。

配置优化工具:

// 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: './src/server/index.js',
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  externals: [nodeExternals()]
}

module.exports = merge(baseConfig, config)

优化之后文件大小为 8KB 左右。

服务器端代码模块化拆分

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

优化代码组织方式,渲染 React 组件的代码也是独立功能,所以把它从服务器端入口文件中进行抽离,方便后期维护。

// src\server\renderer.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from '../share/pages/Home'

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

// src/server/index.js
import app from './http'
import renderer from './renderer'

app.get('/', (req, res) => {
  res.send(renderer())
})

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值