面試官:說說React服務端渲染怎麼做?原理是什麼?

發表於2023-09-24

關注公眾號

關注公眾號獲取程式碼以及最新教程和文章,也可以聯絡作者,獲取幫助

在這裡插入圖片描述

一、SSR

在SSR中,我們瞭解到Server-Side Rendering ,簡稱SSR,意為服務端渲染

指由服務側完成頁面的 HTML 結構拼接的頁面處理技術,傳送到瀏覽器,然後為其繫結狀態與事件,成為完全可互動頁面的過程
在這裡插入圖片描述

其解決的問題主要有兩個:

  • SEO,由於搜尋引擎爬蟲抓取工具可以直接檢視完全渲染的頁面
  • 加速首屏載入,解決首屏白屏問題

二、如何做

react中,實現SSR主要有兩種形式:

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

這裡主要以手動搭建一個SSR框架進行實現

首先透過express啟動一個app.js檔案,用於監聽3000埠的請求,當請求根目錄時,返回HTML,如下:

const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
       Hello world
   </body>
</html>
`))

app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))

然後再伺服器中編寫react程式碼,在app.js中進行應引用

import React from 'react'

const Home = () =>{

    return <div>home</div>

}

export default Home

為了讓伺服器能夠識別JSX,這裡需要使用webpakc對專案進行打包轉換,建立一個配置檔案webpack.server.js並進行相關配置,如下:

const path = require('path')    //node的path模組
const nodeExternals = require('webpack-node-externals')

module.exports = {
    target:'node',
    mode:'development',           //開發模式
    entry:'./app.js',             //入口
    output: {                     //打包出口
        filename:'bundle.js',     //打包後的檔名
        path:path.resolve(__dirname,'build')    //存放到根目錄的build資料夾
    },
    externals: [nodeExternals()],  //保持node中require的引用方式
    module: {
        rules: [{                  //打包規則
           test:   /\.js?$/,       //對所有js檔案進行打包
           loader:'babel-loader',  //使用babel-loader進行打包
           exclude: /node_modules/,//不打包node_modules中的js檔案
           options: {
               presets: ['react','stage-0',['env', { 
                                  //loader時額外的打包規則,對react,JSX,ES6進行轉換
                    targets: {
                        browsers: ['last 2versions']   //對主流瀏覽器最近兩個版本進行相容
                    }
               }]]
           }
       }]
    }
}

接著藉助react-dom提供了服務端渲染的 renderToString方法,負責把React元件解析成html

import express from 'express'
import React from 'react'//引入React以支援JSX的語法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import Home from'./src/containers/Home'

const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
        ${content}
   </body>
</html>
`))

app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

上面的過程中,已經能夠成功將元件渲染到了頁面上

但是像一些事件處理的方法,是無法在服務端完成,因此需要將元件程式碼在瀏覽器中再執行一遍,這種伺服器端和客戶端共用一套程式碼的方式就稱之為同構

重構通俗講就是一套React程式碼在伺服器上執行一遍,到達瀏覽器又執行一遍:

  • 服務端渲染完成頁面結構
  • 瀏覽器端渲染完成事件繫結

瀏覽器實現事件繫結的方式為讓瀏覽器去拉取JS檔案執行,讓JS程式碼來控制,因此需要引入script標籤

透過script標籤為頁面引入客戶端執行的react程式碼,並透過expressstatic中介軟體為js檔案配置路由,修改如下:

import express from 'express'
import React from 'react'//引入React以支援JSX的語法
import { renderToString } from'react-dom/server'//引入renderToString方法
import Home from './src/containers/Home'
 
const app = express()
app.use(express.static('public'));
//使用express提供的static中介軟體,中介軟體會將所有靜態檔案的路由指向public資料夾
 const content = renderToString(<Home/>)
 
app.get('/',(req,res)=>res.send(`
<html>
   <head>
       <title>ssr demo</title>
   </head>
   <body>
        ${content}
   <script src="/index.js"></script>
   </body>
</html>
`))

 app.listen(3001, () =>console.log('Example app listening on port 3001!'))

然後再客戶端執行以下react程式碼,新建webpack.client.js作為客戶端React程式碼的webpack配置檔案如下:

const path = require('path')                    //node的path模組

module.exports = {
    mode:'development',                         //開發模式
    entry:'./src/client/index.js',              //入口
    output: {                                   //打包出口
        filename:'index.js',                    //打包後的檔名
        path:path.resolve(__dirname,'public')   //存放到根目錄的build資料夾
    },
    module: {
        rules: [{                               //打包規則
           test:   /\.js?$/,                    //對所有js檔案進行打包
           loader:'babel-loader',               //使用babel-loader進行打包
           exclude: /node_modules/,             //不打包node_modules中的js檔案
           options: {
               presets: ['react','stage-0',['env', {     
                    //loader時額外的打包規則,這裡對react,JSX進行轉換
                    targets: {
                        browsers: ['last 2versions']   //對主流瀏覽器最近兩個版本進行相容
                    }
               }]]
           }
       }]
    }
}

這種方法就能夠簡單實現首頁的react服務端渲染,過程對應如下圖:

在這裡插入圖片描述

在做完初始渲染的時候,一個應用會存在路由的情況,配置資訊如下:

import React from 'react'                   //引入React以支援JSX
import { Route } from 'react-router-dom'    //引入路由
import Home from './containers/Home'        //引入Home元件

export default (
    <div>
        <Route path="/" exact component={Home}></Route>
    </div>
)

然後可以透過index.js引用路由資訊,如下:

import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'

const App= () => {
    return (
        <BrowserRouter>
           {Router}
        </BrowserRouter>
    )
}

ReactDom.hydrate(<App/>, document.getElementById('root'))

這時候控制檯會存在報錯資訊,原因在於每個Route元件外面包裹著一層div,但服務端返回的程式碼中並沒有這個div

解決方法只需要將路由資訊在服務端執行一遍,使用使用StaticRouter來替代BrowserRouter,透過context進行引數傳遞

import express from 'express'
import React from 'react'//引入React以支援JSX的語法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
 
const app = express()
app.use(express.static('public'));
//使用express提供的static中介軟體,中介軟體會將所有靜態檔案的路由指向public資料夾

app.get('/',(req,res)=>{
    const content  = renderToString((
        //傳入當前path
        //context為必填引數,用於服務端渲染引數傳遞
        <StaticRouter location={req.path} context={{}}>
           {Router}
        </StaticRouter>
    ))
    res.send(`
   <html>
       <head>
           <title>ssr demo</title>
       </head>
       <body>
       <div id="root">${content}</div>
       <script src="/index.js"></script>
       </body>
   </html>
    `)
})


app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

這樣也就完成了路由的服務端渲染

三、原理

整體react服務端渲染原理並不複雜,具體如下:

node server 接收客戶端請求,得到當前的請求url 路徑,然後在已有的路由表內查詢到對應的元件,拿到需要請求的資料,將資料作為 propscontext或者store 形式傳入元件

然後基於 react 內建的服務端渲染方法 renderToString()把元件渲染為 html字串在把最終的 html 進行輸出前需要將資料注入到瀏覽器端

瀏覽器開始進行渲染和節點對比,然後執行完成元件內事件繫結和一些互動,瀏覽器重用了服務端輸出的 html 節點,整個流程結束

參考文獻

相關文章