React服務(wù)端渲染(SSR)的實(shí)現(xiàn)方式和最佳實(shí)踐
一、什么是服務(wù)端渲染?
1.1 客戶端渲染(CSR) vs 服務(wù)端渲染(SSR)
客戶端渲染(Client-Side Rendering, CSR):
- 瀏覽器請(qǐng)求HTML文檔
- 服務(wù)器返回空的HTML框架和JavaScript bundle
- 瀏覽器下載并執(zhí)行JavaScript
- React在客戶端渲染內(nèi)容
服務(wù)端渲染(Server-Side Rendering, SSR):
- 瀏覽器請(qǐng)求HTML文檔
- 服務(wù)器執(zhí)行React組件并生成HTML
- 服務(wù)器返回完整的HTML內(nèi)容
- 瀏覽器立即顯示內(nèi)容,然后加載JavaScript
- React在客戶端"激活"(hydrate)頁(yè)面
1.2 SSR的優(yōu)勢(shì)
- 更好的SEO: 搜索引擎可以直接抓取完整內(nèi)容
- 更快的首屏加載: 用戶無(wú)需等待JS加載就能看到內(nèi)容
- 更好的性能: 特別是對(duì)于低性能設(shè)備和慢網(wǎng)絡(luò)
- 社交分享優(yōu)化: 社交媒體的爬蟲可以正確獲取頁(yè)面元數(shù)據(jù)
二、React SSR的核心原理
2.1 SSR工作原理流程圖

2.2 關(guān)鍵技術(shù)概念
- 渲染ToString: 將React組件轉(zhuǎn)換為HTML字符串
- 同構(gòu)應(yīng)用: 同一套代碼在服務(wù)器和客戶端運(yùn)行
- Hydration(水合): React在客戶端"接管"服務(wù)端渲染的靜態(tài)頁(yè)面
- 狀態(tài)同步: 確保服務(wù)器和客戶端的狀態(tài)一致
三、手動(dòng)實(shí)現(xiàn)React SSR
3.1 項(xiàng)目結(jié)構(gòu)
ssr-project/ ├── src/ │ ├── client/ # 客戶端代碼 │ │ └── index.js # 客戶端入口 │ ├── server/ # 服務(wù)器代碼 │ │ └── index.js # 服務(wù)器入口 │ └── shared/ # 共享代碼 │ ├── App.js # 主應(yīng)用組件 │ ├── routes.js # 路由配置 │ └── components/ # 共享組件 ├── public/ # 靜態(tài)資源 └── webpack.config.js # Webpack配置
3.2 服務(wù)端代碼實(shí)現(xiàn)
server/index.js:
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import App from '../shared/App';
import serialize from 'serialize-javascript';
const app = express();
const PORT = process.env.PORT || 3000;
// 提供靜態(tài)文件服務(wù)
app.use(express.static('public'));
// 處理所有路由
app.get('*', (req, res) => {
// 創(chuàng)建路由上下文(用于重定向等)
const context = {};
// 渲染組件為HTML字符串
const markup = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
// 檢查是否重定向
if (context.url) {
return res.redirect(301, context.url);
}
// 構(gòu)建完整HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<title>React SSR App</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/styles.css" rel="external nofollow" >
</head>
<body>
<div id="root">${markup}</div>
<script>
window.__INITIAL_STATE__ = ${serialize({})}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
3.3 客戶端代碼實(shí)現(xiàn)
client/index.js:
import React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from '../shared/App';
// 獲取服務(wù)端注入的初始狀態(tài)
const initialState = window.__INITIAL_STATE__ || {};
// Hydrate應(yīng)用
hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
3.4 共享組件
shared/App.js:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import NotFound from './components/NotFound';
const App = () => {
return (
<div className="app">
<header>
<h1>React SSR Example</h1>
</header>
<main>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route component={NotFound} />
</Switch>
</main>
</div>
);
};
export default App;
四、數(shù)據(jù)獲取與狀態(tài)管理
4.1 服務(wù)端數(shù)據(jù)獲取
實(shí)現(xiàn)服務(wù)端數(shù)據(jù)預(yù)?。?/strong>
// shared/components/Home.js
import React, { useEffect, useState } from 'react';
const Home = () => {
const [data, setData] = useState(null);
// 客戶端數(shù)據(jù)獲取
useEffect(() => {
if (!window.__INITIAL_DATA__) {
fetchData().then(setData);
}
}, []);
// 使用服務(wù)端注入的數(shù)據(jù)或客戶端獲取的數(shù)據(jù)
const displayData = data || window.__INITIAL_DATA__;
return (
<div>
<h2>Home Page</h2>
{displayData ? (
<div>{/* 渲染數(shù)據(jù) */}</div>
) : (
<div>Loading...</div>
)}
</div>
);
};
// 靜態(tài)方法用于服務(wù)端數(shù)據(jù)獲取
Home.fetchData = async () => {
// 這里模擬API調(diào)用
const response = await fetch('/api/data');
return response.json();
};
export default Home;
更新服務(wù)器代碼以支持?jǐn)?shù)據(jù)預(yù)取:
// server/index.js
import { matchPath } from 'react-router-dom';
import routes from '../shared/routes';
app.get('*', async (req, res) => {
const context = {};
// 查找匹配的路由
const matchedRoute = routes.find(route =>
matchPath(req.url, route)
);
// 執(zhí)行數(shù)據(jù)獲取方法(如果存在)
let initialData = {};
if (matchedRoute && matchedRoute.component.fetchData) {
initialData = await matchedRoute.component.fetchData();
}
const markup = renderToString(
<StaticRouter location={req.url} context={context}>
<App initialData={initialData} />
</StaticRouter>
);
const html = `
<!DOCTYPE html>
<html>
<head>
<title>React SSR App</title>
</head>
<body>
<div id="root">${markup}</div>
<script>
window.__INITIAL_DATA__ = ${serialize(initialData)}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
五、使用Next.js簡(jiǎn)化SSR
5.1 Next.js簡(jiǎn)介
Next.js是一個(gè)流行的React框架,內(nèi)置了SSR支持,簡(jiǎn)化了配置和開發(fā)流程。
5.2 創(chuàng)建Next.js應(yīng)用
npx create-next-app@latest my-ssr-app cd my-ssr-app npm run dev
5.3 Next.js頁(yè)面和路由
pages/index.js (自動(dòng)路由):
import React from 'react';
// Next.js自動(dòng)處理SSR
const HomePage = ({ data }) => {
return (
<div>
<h1>Home Page</h1>
<p>Data: {data}</p>
</div>
);
};
// 服務(wù)端數(shù)據(jù)獲取
export async function getServerSideProps() {
// 在服務(wù)器上運(yùn)行
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: {
data
}
};
}
export default HomePage;
5.4 靜態(tài)生成(SSG)與服務(wù)器渲染(SSR)
靜態(tài)生成(SSG - 構(gòu)建時(shí)獲取數(shù)據(jù)):
export async function getStaticProps() {
// 在構(gòu)建時(shí)運(yùn)行
return {
props: {
data: // 從CMS、文件系統(tǒng)等獲取數(shù)據(jù)
}
};
}
服務(wù)器渲染(SSR - 請(qǐng)求時(shí)獲取數(shù)據(jù)):
export async function getServerSideProps(context) {
// 每次請(qǐng)求時(shí)運(yùn)行
return {
props: {
data: // 根據(jù)請(qǐng)求參數(shù)獲取數(shù)據(jù)
}
};
}
六、高級(jí)SSR主題
6.1 代碼分割與懶加載
使用React.lazy和Suspense:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
};
服務(wù)器端支持懶加載:
// 需要使用@loadable/components等庫(kù)支持SSR的代碼分割
import loadable from '@loadable/component';
const LazyComponent = loadable(() => import('./LazyComponent'), {
fallback: <div>Loading...</div>
});
6.2 狀態(tài)管理(Redux)與SSR
創(chuàng)建store工廠函數(shù):
// shared/store/configureStore.js
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
export default function configureStore(preloadedState) {
return createStore(
rootReducer,
preloadedState,
applyMiddleware(/* middleware */)
);
}
服務(wù)端store配置:
// server/index.js
app.get('*', async (req, res) => {
const store = configureStore();
// 執(zhí)行需要的數(shù)據(jù)獲取操作
await Promise.all(
matchedRoutes.map(route => {
return route.component.fetchData
? route.component.fetchData(store)
: Promise.resolve(null);
})
);
const markup = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
</Provider>
);
// 將store狀態(tài)傳遞給客戶端
const initialState = store.getState();
const html = `
<script>
window.__INITIAL_STATE__ = ${serialize(initialState)}
</script>
`;
});
客戶端store配置:
// client/index.js
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
hydrate(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
6.3 SEO與元標(biāo)簽管理
使用React Helmet管理頭部標(biāo)簽:
import { Helmet } from 'react-helmet';
const SEOComponent = ({ title, description }) => {
return (
<div>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
</Helmet>
{/* 頁(yè)面內(nèi)容 */}
</div>
);
};
服務(wù)端渲染頭部:
// 在服務(wù)器端
const helmet = Helmet.renderStatic();
const html = `
<html>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body>
<div id="root">${markup}</div>
</body>
</html>
`;
七、性能優(yōu)化與最佳實(shí)踐
7.1 緩存策略
組件級(jí)別緩存:
import lruCache from 'lru-cache';
// 創(chuàng)建緩存實(shí)例
const ssrCache = new lruCache({
max: 100, // 最大緩存項(xiàng)數(shù)
maxAge: 1000 * 60 * 15 // 15分鐘
});
// 緩存渲染結(jié)果
app.get('*', (req, res) => {
const key = req.url;
if (ssrCache.has(key)) {
return res.send(ssrCache.get(key));
}
// 渲染組件
const html = renderToString(<App />);
// 緩存結(jié)果
ssrCache.set(key, html);
res.send(html);
});
7.2 流式渲染
使用renderToNodeStream提升性能:
import { renderToNodeStream } from 'react-dom/server';
app.get('*', (req, res) => {
// 先發(fā)送HTML頭部
res.write(`
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root">
`);
// 創(chuàng)建渲染流
const stream = renderToNodeStream(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
);
// 管道傳輸?shù)巾憫?yīng)
stream.pipe(res, { end: false });
// 流結(jié)束時(shí)完成HTML
stream.on('end', () => {
res.write(`
</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
res.end();
});
});
7.3 錯(cuò)誤處理
組件錯(cuò)誤邊界:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 記錄錯(cuò)誤到日志服務(wù)
console.error('SSR Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
服務(wù)器錯(cuò)誤處理:
app.get('*', async (req, res, next) => {
try {
// 渲染邏輯
} catch (error) {
console.error('SSR Error:', error);
// 返回錯(cuò)誤頁(yè)面或回退到CSR
res.send(`
<html>
<body>
<div id="root">
<h1>Server Error</h1>
<p>Please try refreshing the page.</p>
</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
}
});
八、常見問題與解決方案
8.1 窗口對(duì)象未定義問題
解決方案:
// 檢查是否在瀏覽器環(huán)境中
if (typeof window !== 'undefined') {
// 使用窗口相關(guān)API
}
// 或者使用動(dòng)態(tài)導(dǎo)入
useEffect(() => {
const loadBrowserOnlyModule = async () => {
const module = await import('./browser-only-module');
// 使用模塊
};
loadBrowserOnlyModule();
}, []);
8.2 樣式處理問題
使用CSS-in-JS庫(kù)的SSR支持:
// 使用styled-components
import { ServerStyleSheet } from 'styled-components';
app.get('*', (req, res) => {
const sheet = new ServerStyleSheet();
try {
const markup = renderToString(
sheet.collectStyles(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
)
);
const styles = sheet.getStyleTags();
const html = `
<html>
<head>${styles}</head>
<body>
<div id="root">${markup}</div>
</body>
</html>
`;
res.send(html);
} finally {
sheet.seal();
}
});
九、總結(jié)
React服務(wù)端渲染是一個(gè)強(qiáng)大的技術(shù),可以顯著提升應(yīng)用性能和用戶體驗(yàn)。實(shí)現(xiàn)SSR需要考慮多個(gè)方面:
- 同構(gòu)應(yīng)用結(jié)構(gòu): 確保代碼在服務(wù)器和客戶端都能運(yùn)行
- 數(shù)據(jù)預(yù)取: 在服務(wù)器上獲取必要數(shù)據(jù)并傳遞給客戶端
- 狀態(tài)同步: 保持服務(wù)器和客戶端狀態(tài)一致
- 性能優(yōu)化: 使用緩存、流式渲染等技術(shù)提升性能
- 錯(cuò)誤處理: 優(yōu)雅處理渲染過程中的錯(cuò)誤
對(duì)于大多數(shù)項(xiàng)目,建議使用成熟的框架如Next.js,它們提供了開箱即用的SSR支持和完善的解決方案。對(duì)于特殊需求或?qū)W習(xí)目的,手動(dòng)實(shí)現(xiàn)SSR可以幫助深入理解其工作原理。
以上就是React服務(wù)端渲染(SSR)的實(shí)現(xiàn)方式和最佳實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于React服務(wù)端渲染(SSR)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React組件中按鈕的loading狀態(tài)失效問題的解決方案
在React開發(fā)過程中,我遇到這樣的情況:頁(yè)面按鈕的loading屬性失效,盡管通過useEffect打印發(fā)現(xiàn)loading狀態(tài)(確實(shí)在true和false之間切換,但按鈕卻沒有表現(xiàn)出預(yù)期的加載效果,所以本文給大家介紹了失效的解決方案,需要的朋友可以參考下2025-07-07
react-native聊天室|RN版聊天App仿微信實(shí)例|RN仿微信界面
這篇文章主要介紹了react-native聊天室|RN版聊天App仿微信實(shí)例|RN仿微信界面,需要的朋友可以參考下2019-11-11
React18中請(qǐng)求數(shù)據(jù)的官方姿勢(shì)適用其他框架
這篇文章主要為大家介紹了官方回答在React18中請(qǐng)求數(shù)據(jù)的正確姿勢(shì)詳解,同樣也適用其他框架,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
如何將你的AngularJS1.x應(yīng)用遷移至React的方法
本篇文章主要介紹了如何將你的AngularJS1.x應(yīng)用遷移至React的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-02-02
淺談React?Refs?使用場(chǎng)景及核心要點(diǎn)
本文主要介紹了React?Refs?使用場(chǎng)景及核心要點(diǎn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06

