说说React服务端渲染怎么做?原理是什么?

时间:2022-06-01 17:47:21

说说React服务端渲染怎么做?原理是什么?

说说React服务端渲染怎么做?原理是什么?

一、是什么

在SSR中,我们了解到Server-Side Rendering ,简称SSR,意为服务端渲染

指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程

说说React服务端渲染怎么做?原理是什么?

其解决的问题主要有两个:

  • SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
  • 加速首屏加载,解决首屏白屏问题

二、如何做

在react中,实现SSR主要有两种形式:

  • 手动搭建一个 SSR 框架
  • 使用成熟的SSR 框架,如 Next.JS

这里主要以手动搭建一个SSR框架进行实现

首先通过express启动一个app.js文件,用于监听3000端口的请求,当请求根目录时,返回HTML,如下:

  1. const express = require('express'
  2. const app = express() 
  3. app.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.  
  14. app.listen(3000, () => console.log('Exampleapp listening on port 3000!')) 

然后再服务器中编写react代码,在app.js中进行应引用

  1. import React from 'react' 
  2.  
  3. const Home = () =>{ 
  4.  
  5.     return <div>home</div> 
  6.  
  7.  
  8. export default Home 

为了让服务器能够识别JSX,这里需要使用webpakc对项目进行打包转换,创建一个配置文件webpack.server.js并进行相关配置,如下:

  1. const path = require('path')    //node的path模块 
  2. const nodeExternals = require('webpack-node-externals'
  3.  
  4. module.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.     } 

接着借助react-dom提供了服务端渲染的 renderToString方法,负责把React组件解析成html

  1. import express from 'express' 
  2. import React from 'react'//引入React以支持JSX的语法 
  3. import { renderToString } from 'react-dom/server'//引入renderToString方法 
  4. import Home from'./src/containers/Home' 
  5.  
  6. const app= express() 
  7. const content = renderToString(<Home/>) 
  8. app.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.  
  19. app.listen(3001, () => console.log('Exampleapp listening on port 3001!')) 

上面的过程中,已经能够成功将组件渲染到了页面上

但是像一些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为「同构」

重构通俗讲就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍:

  • 服务端渲染完成页面结构
  • 浏览器端渲染完成事件绑定

浏览器实现事件绑定的方式为让浏览器去拉取JS文件执行,让JS代码来控制,因此需要引入script标签

通过script标签为页面引入客户端执行的react代码,并通过express的static中间件为js文件配置路由,修改如下:

  1. import express from 'express' 
  2. import React from 'react'//引入React以支持JSX的语法 
  3. import { renderToString } from'react-dom/server'//引入renderToString方法 
  4. import Home from './src/containers/Home' 
  5.   
  6. const app = express() 
  7. app.use(express.static('public')); 
  8. //使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 
  9.  const content = renderToString(<Home/>) 
  10.   
  11. app.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配置文件如下:

  1. const path = require('path')                    //node的path模块 
  2.  
  3. module.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.     } 

这种方法就能够简单实现首页的react服务端渲染,过程对应如下图:

说说React服务端渲染怎么做?原理是什么?

在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下:

  1. import React from 'react'                   //引入React以支持JSX 
  2. import { Route } from 'react-router-dom'    //引入路由 
  3. import Home from './containers/Home'        //引入Home组件 
  4.  
  5. export default ( 
  6.     <div> 
  7.         <Route path="/" exact component={Home}></Route> 
  8.     </div> 

然后可以通过index.js引用路由信息,如下:

  1. import React from 'react' 
  2. import ReactDom from 'react-dom' 
  3. import { BrowserRouter } from'react-router-dom' 
  4. import Router from'../Routers' 
  5.  
  6. const App= () => { 
  7.     return ( 
  8.         <BrowserRouter> 
  9.            {Router} 
  10.         </BrowserRouter> 
  11.     ) 
  12.  
  13. ReactDom.hydrate(<App/>, document.getElementById('root')) 

这时候控制台会存在报错信息,原因在于每个Route组件外面包裹着一层div,但服务端返回的代码中并没有这个div

解决方法只需要将路由信息在服务端执行一遍,使用使用StaticRouter来替代BrowserRouter,通过context进行参数传递

  1. import express from 'express' 
  2. import React from 'react'//引入React以支持JSX的语法 
  3. import { renderToString } from 'react-dom/server'//引入renderToString方法 
  4. import { StaticRouter } from 'react-router-dom' 
  5. import Router from '../Routers' 
  6.   
  7. const app = express() 
  8. app.use(express.static('public')); 
  9. //使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 
  10.  
  11. app.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.  
  33. app.listen(3001, () => console.log('Exampleapp listening on port 3001!')) 

这样也就完成了路由的服务端渲染

三、原理

整体react服务端渲染原理并不复杂,具体如下:

node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件

然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html进行输出前需要将数据注入到浏览器端

浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束

参考文献

  • https://zhuanlan.zhihu.com/p/52693113
  • https://segmentfault.com/a/1190000020417285
  • https://juejin.cn/post/6844904000387563533#heading-14

原文地址:https://mp.weixin.qq.com/s/-5XZXiraioUTwNl-6zU3hg