参考答案:
服务端渲染(Server-Side Rendering
,简称SSR
),指由服务侧完成页面的 HTML
结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
其解决的问题主要有两个:
在react
中,实现SSR
主要有两种形式:
这里主要以手动搭建一个SSR
框架进行实现
首先通过express
启动一个app.js
文件,用于监听3000端口的请求,当请求根目录时,返回HTML
,如下:
1const express = require('express') 2const app = express() 3app.get('/', (req,res) => res.send(` 4<html> 5 <head> 6 <title>ssr demo</title> 7 </head> 8 <body> 9 Hello world 10 </body> 11</html> 12`)) 13 14app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))
然后再服务器中编写react
代码,在app.js
中进行应引用
1import React from 'react' 2 3const Home = () =>{ 4 5 return <div>home</div> 6 7} 8 9export default Home
为了让服务器能够识别JSX
,这里需要使用webpakc
对项目进行打包转换,创建一个配置文件webpack.server.js
并进行相关配置,如下:
1const path = require('path') //node的path模块 2const nodeExternals = require('webpack-node-externals') 3 4module.exports = { 5 target:'node', 6 mode:'development', //开发模式 7 entry:'./app.js', //入口 8 output: { //打包出口 9 filename:'bundle.js', //打包后的文件名 10 path:path.resolve(__dirname,'build') //存放到根目录的build文件夹 11 }, 12 externals: [nodeExternals()], //保持node中require的引用方式 13 module: { 14 rules: [{ //打包规则 15 test: /\.js?$/, //对所有js文件进行打包 16 loader:'babel-loader', //使用babel-loader进行打包 17 exclude: /node_modules/,//不打包node_modules中的js文件 18 options: { 19 presets: ['react','stage-0',['env', { 20 //loader时额外的打包规则,对react,JSX,ES6进行转换 21 targets: { 22 browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容 23 } 24 }]] 25 } 26 }] 27 } 28}
接着借助react-dom
提供了服务端渲染的 renderToString
方法,负责把React
组件解析成html
1import express from 'express' 2import React from 'react'//引入React以支持JSX的语法 3import { renderToString } from 'react-dom/server'//引入renderToString方法 4import Home from'./src/containers/Home' 5 6const app= express() 7const content = renderToString(<Home/>) 8app.get('/',(req,res) => res.send(` 9<html> 10 <head> 11 <title>ssr demo</title> 12 </head> 13 <body> 14 ${content} 15 </body> 16</html> 17`)) 18 19app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
上面的过程中,已经能够成功将组件渲染到了页面上
但是像一些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为同构
通俗讲,“同构”就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍:
浏览器实现事件绑定的方式为让浏览器去拉取JS
文件执行,让JS
代码来控制,因此需要引入script
标签
通过script
标签为页面引入客户端执行的react
代码,并通过express
的static
中间件为js
文件配置路由,修改如下:
1import express from 'express' 2import React from 'react'//引入React以支持JSX的语法 3import { renderToString } from'react-dom/server'//引入renderToString方法 4import Home from './src/containers/Home' 5 6const app = express() 7app.use(express.static('public')); 8//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 9 const content = renderToString(<Home/>) 10 11app.get('/',(req,res)=>res.send(` 12<html> 13 <head> 14 <title>ssr demo</title> 15 </head> 16 <body> 17 ${content} 18 <script src="/index.js"></script> 19 </body> 20</html> 21`)) 22 23 app.listen(3001, () =>console.log('Example app listening on port 3001!'))
然后再客户端执行以下react
代码,新建webpack.client.js
作为客户端React代码的webpack
配置文件如下:
1const path = require('path') //node的path模块 2 3module.exports = { 4 mode:'development', //开发模式 5 entry:'./src/client/index.js', //入口 6 output: { //打包出口 7 filename:'index.js', //打包后的文件名 8 path:path.resolve(__dirname,'public') //存放到根目录的build文件夹 9 }, 10 module: { 11 rules: [{ //打包规则 12 test: /\.js?$/, //对所有js文件进行打包 13 loader:'babel-loader', //使用babel-loader进行打包 14 exclude: /node_modules/, //不打包node_modules中的js文件 15 options: { 16 presets: ['react','stage-0',['env', { 17 //loader时额外的打包规则,这里对react,JSX进行转换 18 targets: { 19 browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容 20 } 21 }]] 22 } 23 }] 24 } 25}
这种方法就能够简单实现首页的react
服务端渲染,过程对应如下图:
在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下:
1import React from 'react' //引入React以支持JSX 2import { Route } from 'react-router-dom' //引入路由 3import Home from './containers/Home' //引入Home组件 4 5export default ( 6 <div> 7 <Route path="/" exact component={Home}></Route> 8 </div> 9)
然后可以通过index.js
引用路由信息,如下:
1import React from 'react' 2import ReactDom from 'react-dom' 3import { BrowserRouter } from'react-router-dom' 4import Router from'../Routers' 5 6const App= () => { 7 return ( 8 <BrowserRouter> 9 {Router} 10 </BrowserRouter> 11 ) 12} 13 14ReactDom.hydrate(<App/>, document.getElementById('root'))
这时候控制台会存在报错信息,原因在于每个Route
组件外面包裹着一层div
,但服务端返回的代码中并没有这个div
解决方法只需要将路由信息在服务端执行一遍,使用使用StaticRouter
来替代BrowserRouter
,通过context
进行参数传递
1import express from 'express' 2import React from 'react'//引入React以支持JSX的语法 3import { renderToString } from 'react-dom/server'//引入renderToString方法 4import { StaticRouter } from 'react-router-dom' 5import Router from '../Routers' 6 7const app = express() 8app.use(express.static('public')); 9//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 10 11app.get('/',(req,res)=>{ 12 const content = renderToString(( 13 //传入当前path 14 //context为必填参数,用于服务端渲染参数传递 15 <StaticRouter location={req.path} context={{}}> 16 {Router} 17 </StaticRouter> 18 )) 19 res.send(` 20 <html> 21 <head> 22 <title>ssr demo</title> 23 </head> 24 <body> 25 <div id="root">${content}</div> 26 <script src="/index.js"></script> 27 </body> 28 </html> 29 `) 30}) 31 32 33app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
这样也就完成了路由的服务端渲染
整体react
服务端渲染原理并不复杂,具体如下:
node server
接收客户端请求,得到当前的请求url
路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props
、context
或者store
形式传入组件
然后基于 react
内置的服务端渲染方法 renderToString()
把组件渲染为 html
字符串在把最终的 html
进行输出前需要将数据注入到浏览器端
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html
节点,整个流程结束
最近更新时间:2024-08-10