關注公眾號
關注公眾號獲取程式碼以及最新教程和文章,也可以聯絡作者,獲取幫助
一、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
程式碼,並透過express
的static
中介軟體為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
路徑,然後在已有的路由表內查詢到對應的元件,拿到需要請求的資料,將資料作為 props
、context
或者store
形式傳入元件
然後基於 react
內建的服務端渲染方法 renderToString()
把元件渲染為 html
字串在把最終的 html
進行輸出前需要將資料注入到瀏覽器端
瀏覽器開始進行渲染和節點對比,然後執行完成元件內事件繫結和一些互動,瀏覽器重用了服務端輸出的 html
節點,整個流程結束