React SSR - 01 SSR 介绍 和 快速开始
React SSR 介绍
什么是客户端渲染
CSR:Client Side Rendering
数据和 HTML 的拼接是在客户端(浏览器)使用 JavaScript 完成的,服务端只需要返回 JSON 数据即可。
使用 React 开发的项目是客户端渲染的单页应用(SPA)。
什么是服务器端渲染
SSR:Server Side Rendering
数据和 HTML 的拼接是在服务器端完成的,客户端向服务器端发送请求,服务器端返回拼接好的 HTML,客户端只需将其显示出来。
客户端渲染存在的问题
- 首屏等待时间长,用户体验差。
- 页面结构为空,不利于 SEO。
CSR 过程
- 用户首次访问页面,客户端向服务器发送请求,服务端返回 HTML,但是 HTML 文档只包含一些 CSS 资源链接和 JS 资源链接,等待期间页面没有展示的内容。
- 浏览器识别 HTML 文档,向服务器请求资源文件,请求过程中页面上没有展示内容。
- 加载完资源文件,就要执行对应的 JS 脚本,在执行 JS 脚本的时候又要向服务器获取当前页面所需要的数据,请求数据的过程中,页面上依然没有展示内容(或者渲染了一些静态内容,但是展示动态数据的地方还是空的)。
- 当数据请求完,在客户端又要使用 JavaScript 把请求的 JSON 数据和 HTML 拼接好,将拼接好的结果显示在页面中。到此页面才算加载完成。
在整个过程中,页面是没有内容或内容不完整的。即首屏等待时间长,用户体验差。
搜索引擎爬虫通常只会解析该请求返回的 HTML 内容,除非是针对性的爬虫,否则会忽略其它资源和请求数据的请求。
而用户请求客户端渲染的页面获取的 HTML 文档,页面结构是空的(浏览器查看页面源代码),所以爬虫抓取不到什么内容,也就不利于 SEO。
SSR 过程
- 用户首次访问页面,客户端向服务器端发送请求,服务器端将数据和 HTML 拼接好的结果返回给客户端。此时浏览器还是处于等待状态。
- 浏览器执行服务器端返回的 HTML 内容,它是包含了服务器端数据的,所以用户可以看到页面内容,只不过现在页面上展示的只有静态的 HTML 内容,没有动效果,浏览器还是要向服务端发送请求,获取资源文件。
- 当请求完 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 实现
实现步骤:
- 引入要渲染的 React 组件
- 通过
renderToString
方法将 React 组件转换为 HTML 字符串 - 将结果 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 节点的开销,只进行事件处理绑定。
hydrate
和 render
的区别就是 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.js
和 webpack.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())
})
React SSR - 01 SSR 介绍 和 快速开始相关推荐
- Jest 学习01 - Jest 介绍、快速体验、vscode 智能提示、配置、监视模式、Babel 配置
起步 测试到底测什么 提到测试的时候,即使是最简单的一个代码块可能都让初学者不知所措.最常问的问题的是"我怎么知道要测试什么?".如果你正在写一个 Web 应用,那么依次测试每个页 ...
- 从零搭建一个基于React+Nextjs的SSR网站(四):如何搭建服务器并部署Nextjs项目
个人博客源码:https://github.com/shaotianyu/blog-front PS: 如果你有疑惑,可以给我留言,咱们一起解决它. 从零搭建一个基于React+Nextjs的SSR网 ...
- 初识 SSR (SSR 入门 对产品经理,运营友好)
前言 最近在大规模的改造SSR页面,所以对 SSR 技术有了一定的技术认识,想着写一篇文章来总结一下对于 SSR 初步的认识.这是一篇对产品经理友好的介绍性文章,具体的实现性技术文章在后头(主要是我们 ...
- React 360 初体验介绍与环境搭建
React 360 初体验介绍 从这章节内容呢,我们来学习并了解下什么是react 360,并使用它来开发一个360度可旋转大屏的案例项目.接下来,我们就一起来逐步揭开它神秘的面纱吧! 我们本章节将会 ...
- React版本的todolist小demo 快速复习React(React)
demo 介绍 该demo 就是帮你快速复习 react 的 事件传值,子组件传值,父传子,事件绑定,交互逻辑,react 语法,该demo有,显示隐藏列表,增加内容,删除todo,双击内容编辑是in ...
- Helm基本介绍及快速入门
文章目录 Helm基本介绍及快速入门 一.Helm基本介绍 Helm简介 Helm 相关组件及概念 二.Helm部署 Helm客户端安装 校验是否安装成功 三.Helm 使用 使用仓库(helm re ...
- vue.js 2.0 官方文档学习笔记 —— 01. vue 介绍
这是我的vue.js 2.0的学习笔记,采取了将官方文档中的代码集中到一个文件的形式.目的是保存下来,方便自己查阅. !官方文档:https://cn.vuejs.org/v2/guide/ 01. ...
- React with Webpack -1: 介绍Helloworld
React with Webpack -1: 介绍&Helloworld node.js 开发之react 学习1 context:node.js 开发的工具和lib发展的很快,in othe ...
- html5学习笔记---01.HTML5介绍,02.HTML5的新特性
2013/6/10 01.HTML5介绍 a.创梦技术qq交流群:CreDream:251572072 -------------------- a.创梦技术qq交流群:CreDream:251572 ...
最新文章
- Yann LeCun专访:我不觉得自己有天分,但是我一直往聪明人堆里钻
- Java之基础(1) - 编程中“为了性能”尽量要做到的一些地方
- 第一次上计算机课日记500,第一次上网课作文500字
- header中Content-Disposition的作用与使用方法
- dig下载_DIG的完整形式是什么?
- 谈Servlet与JSP
- ubuntu中文输入法fcitx的安装以及出现方块的解决方法
- MongoDb和LINQ:如何汇总和加入集合
- WinForm------如何修改PanelControl控件背景色
- 华为Linux笔记本拆机,华为MateBook D怎么拆机?华为MateBook D拆机图文步骤详解
- 美国卡内基梅隆大学计算机排名,卡内基梅隆大学,美国卡梅基梅隆大学世界排名?...
- 教妹学 Java:晦涩难懂的泛型
- 阿里双十一,3分01秒破百亿;乐视网称贾跃亭无力履行承诺;法乐第未来宣布解职CFO和CTO丨价值早报
- 武田以3.22亿美元剥离中国大陆非核心业务至海森
- Cathy学习Java——反射和类的加载
- 网易云发送验证码短信,发送通知短信,java版
- 数据中台总体技术架构
- 深入理解 WIN32 PE 文件格式
- AI顶会ACL发榜,腾讯30篇论文入选
- Python爬取百度图片搜索结果