什麼是服務器端渲染?如何使用React進行服務器端渲染?
服務器端渲染,也被稱為SSR,是指JavaScript應用程序能夠在服務器上渲染,而不是在瀏覽器中渲染。
我們為什麼需要這樣做?
- 它可以讓您的網站在首次加載時更快,這對於良好的用戶體驗非常重要。
- 對於SEO來說至關重要:搜索引擎無法(尚未?)有效且正確地索引僅在客戶端渲染的應用程序。儘管Google在索引方面進行了最新的改進,但還有其他搜索引擎,而且Google在任何情況下都不是完美的。此外,Google青睞加載速度快的站點,而需要客戶端加載對於速度來說並不理想。
- 當人們在社交媒體上分享您網站的頁面時,它非常好用,因為他們可以輕鬆收集到所需的元數據以便分享鏈接(圖片、標題、描述等)。
沒有服務器端渲染,您的服務器只是發送一個沒有正文的HTML頁面,僅帶有一些腳本標記,然後由瀏覽器用於渲染應用程序。客戶端渲染的應用程序在首次頁面加載後的任何後續用戶交互方面都很出色。服務器端渲染允許我們在客戶端渲染應用程序和後端渲染應用程序之間找到最佳點:頁面在服務器端生成,但一旦加載完成後所有與頁面的交互都由客戶端處理。
然而,服務器端渲染也有缺點:
- 可以說,服務器端渲染的簡單證明非常簡單,但隨著應用程序的複雜性增加,服務器端渲染的複雜性也會增加。
- 服務器端渲染一個大型應用程序可能需要相當多的資源,並且在負載過重時,甚至可能提供比客戶端渲染更慢的體驗,因為您有一個單一的瓶頸。
以下是服務器端渲染React應用程序所需的非常簡單的示例:
SSR的設置可能會變得非常複雜,大多教程都會從一開始就包含Redux、React Router和許多其他概念。
要理解SSR的工作原理,讓我們從實施一個概念證明的基礎知識開始。
如果您只想查看提供SSR的庫,可以跳過這一段,不必擔心進行基礎工作。
為了實現基本的SSR,我們將使用Express。
如果您對Express不熟悉,或者需要進行一些補充,請查看我的免費Express手冊:https://flaviocopes.com/page/ebooks/
警告:服務器端渲染的複雜性可能會隨著應用程序的複雜性增加而增加。這只是渲染基本React應用程序所需的最低設置。對於更複雜的需求,您可能需要進行更多的工作,或者還可以檢查React的SSR庫。
我假設您使用create-react-app
開始了一個React應用程序。如果您只是嘗試,請使用npx create-react-app ssr
來安裝一個。
在終端中進入主要應用程序文件夾,然後運行以下命令:
npm install express
應用程序目錄中有一組文件夾。創建一個名為server
的新文件夾,然後進入其中並創建一個名為server.js
的文件。
根據create-react-app
的規定,應用程序位於src/App.js
文件中。我們將加載該組件,並使用ReactDOMServer.renderToString()
將其渲染為字符串,該方法由react-dom
提供。
您可以獲取./build/index.html
文件的內容,並將默認情況下應用程序鉤住的標記<div id="root"></div>
替換為${ReactDOMServer.renderToString()}
。build文件夾中的所有內容都將以靜態方式由Express提供。
import path from 'path'
import fs from 'fs'
import express from 'express'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from '../src/App'
const PORT = 8080
const app = express()
const router = express.Router()
const serverRenderer = (req, res, next) => {
fs.readFile(path.resolve('./build/index.html'), 'utf8', (err, data) => {
if (err) {
console.error(err)
return res.status(500).send('An error occurred')
}
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
)
})
}
router.use('^/$', serverRenderer)
router.use(
express.static(path.resolve(\_\_dirname, '..', 'build'), { maxAge: '30d' })
)
// 告訴應用程序使用上述規則
app.use(router)
// app.use(express.static('./build'))
app.listen(PORT, () => {
console.log(`運行在端口 ${PORT} 的SSR`)
})
現在,在客戶端應用程序中,在您的src/index.js
中,不要調用ReactDOM.render()
,改為調用ReactDOM.hydrate()
,它是相同的方法,但在React加載完成後還有額外的能力來綁定事件監聽器到現有的標記上:
ReactDOM.hydrate(<App />, document.getElementById('root'))
所有Node.js代碼都需要由Babel轉譯,因為服務器端的Node.js代碼對JSX一無所知,也不知道ES模塊(我們用於include
語句)。
安裝以下4個包:
npm install @babel/register @babel/preset-env @babel/preset-react ignore-styles
ignore-styles
是一個Babel實用工具,它告訴Babel使用import
語法引入的CSS文件。
讓我們在server/index.js
中創建一個入口點:
require('ignore-styles')
require('@babel/register')({
ignore: [/(node\_modules)/],
presets: ['@babel/preset-env', '@babel/preset-react']
})
require('./server')
構建React應用程序,以便填充build/文件夾:
npm run build
然後運行:
node server/index.js
我說這只是一種簡單的方法,確實如此:
- 在使用import時,它無法正確處理圖片的渲染,這需要Webpack來工作(這使整個過程變得更為複雜)。
- 它無法處理頁面標題元數據,這對於SEO和社交分享非常重要(除其他外)。
因此,儘管這是使用ReactDOMServer.renderToString()
和ReactDOM.hydrate
獲取此基本服務器端渲染的好例子,但對於真正的使用來說不夠用。
使用庫進行服務器端渲染
服務器端渲染很難正確實現,React並沒有實現它的標準方法。
是否值得麻煩、複雜和開銷來實現並獲得好處,而不是使用其他技術來提供這些頁面,仍然有很大爭議。 Reddit上的這場討論就涉及了很多關於這一點的意見。
當服務器端渲染是一個重要問題時,我建議依賴於從一開始就考慮到這一目標的預製庫和工具。