SSR生命周期與實現詳細解答
19. 如果不使用框架,如何從零用React/Vue+Node.js實現一個簡單的SSR應用?
React + Node.js SSR實現步驟:
-
項目結構搭建
/project/client - 客戶端代碼/server - 服務端代碼/shared - 共享代碼
-
服務端基礎設置
// server/index.js const express = require('express'); const React = require('react'); const ReactDOMServer = require('react-dom/server'); const App = require('../shared/App').default;const app = express();app.get('/', (req, res) => {const html = ReactDOMServer.renderToString(<App />);res.send(`<!DOCTYPE html><html><head><title>SSR App</title></head><body><div id="root">${html}</div><script src="/client.bundle.js"></script></body></html>`); });app.listen(3000);
-
客戶端hydrate
// client/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from '../shared/App';ReactDOM.hydrate(<App />, document.getElementById('root'));
-
共享組件
// shared/App.js import React from 'react';const App = () => (<div><h1>Hello SSR</h1></div> );export default App;
-
Webpack配置
- 客戶端配置:target: ‘web’
- 服務端配置:target: ‘node’
Vue + Node.js SSR實現步驟:
-
服務端入口
const Vue = require('vue'); const renderer = require('vue-server-renderer').createRenderer(); const express = require('express'); const app = express();app.get('/', (req, res) => {const vm = new Vue({template: '<div>Hello SSR</div>'});renderer.renderToString(vm, (err, html) => {res.send(`<!DOCTYPE html><html><head><title>Vue SSR</title></head><body>${html}</body></html>`);}); });
-
客戶端入口
import Vue from 'vue'; import App from './App.vue';new Vue({el: '#app',render: h => h(App) });
20. ReactDOMServer.renderToString()的作用是什么?
ReactDOMServer.renderToString()
是React提供的服務端渲染API,它將React組件渲染為靜態HTML字符串。主要作用包括:
- 初始渲染:在服務器端生成完整的HTML結構,包含組件初始狀態的渲染結果
- SEO優化:搜索引擎可以直接抓取已渲染的HTML內容
- 首屏性能:用戶能立即看到已渲染的內容,無需等待JS加載執行
- hydration基礎:為后續客戶端hydrate提供標記點
工作原理:
- 遞歸遍歷React組件樹
- 生成對應的HTML字符串
- 不包含事件處理等交互邏輯
- 保留data-reactid等屬性用于客戶端hydrate
特點:
- 同步操作,會阻塞事件循環直到渲染完成
- 不支持組件生命周期方法(如componentDidMount)
- 不支持refs
- 生成的HTML不包含客戶端交互邏輯
與renderToStaticMarkup()的區別:
- renderToString會添加額外的React內部使用的DOM屬性
- renderToStaticMarkup生成更干凈的HTML,但不支持hydrate
21. 服務端如何構建一個完整的HTML響應?
構建完整HTML響應的關鍵步驟:
-
基本HTML結構
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>SSR App</title>${styles}</head><body><div id="root">${appHtml}</div><script src="${clientBundle}"></script></body></html> `;
-
動態注入內容
- 使用模板引擎(如EJS、Pug)
- 或字符串拼接方式插入變量
-
處理資源路徑
const assets = require('./assets.json'); // webpack生成的asset manifest const styles = `<link href="${assets.client.css}" rel="stylesheet">`; const clientBundle = `<script src="${assets.client.js}"></script>`;
-
狀態脫水(State Dehydration)
const preloadedState = serializeState(store.getState()); const stateScript = `<script>window.__PRELOADED_STATE__ = ${preloadedState}</script>`;
-
完整示例
function renderFullPage(html, preloadedState, styles) {return `<!DOCTYPE html><html><head><title>My App</title>${styles}</head><body><div id="root">${html}</div><script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}</script><script src="/static/client.bundle.js"></script></body></html>`; }
22. 如何在服務端處理頁面的和標簽?
React解決方案:
-
使用react-helmet
import { Helmet } from 'react-helmet';const App = () => (<div><Helmet><title>Page Title</title><meta name="description" content="Page description" /></Helmet>{/* ... */}</div> );// 服務端渲染后獲取head內容 const helmet = Helmet.renderStatic(); const head = `${helmet.title.toString()}${helmet.meta.toString()} `;
-
手動管理
const pageMeta = {title: 'Custom Title',description: 'Custom Description' };// 通過context傳遞 <App meta={pageMeta} />// 組件內使用 const App = ({ meta }) => (<div><head><title>{meta.title}</title><meta name="description" content={meta.description} /></head></div> );
Vue解決方案:
-
使用vue-meta
// 組件中 export default {metaInfo: {title: 'My Page',meta: [{ name: 'description', content: 'My description' }]} }// 服務端渲染 const meta = app.$meta(); const html = `<html><head>${meta.inject().title.text()}</head><body>...</body></html> `;
-
動態路由匹配
// 根據路由配置匹配meta const matchedComponents = router.getMatchedComponents(to); const meta = matchedComponents.reduce((meta, component) => {return Object.assign(meta, component.meta || {}); }, {});
23. 在服務端渲染時,如何處理CSS樣式?
1. CSS Modules
服務端處理:
- 使用webpack的css-loader處理CSS Modules
- 提取類名映射關系
// webpack.config.js (server)
{test: /\.css$/,use: [{loader: 'css-loader',options: {modules: true,exportOnlyLocals: true // 服務端只導出類名映射}}]
}
客戶端處理:
- 正常打包CSS文件
- 使用style-loader或mini-css-extract-plugin提取CSS
2. CSS-in-JS (styled-components, emotion)
styled-components示例:
import { ServerStyleSheet } from 'styled-components';// 服務端渲染
const sheet = new ServerStyleSheet();
const html = ReactDOMServer.renderToString(sheet.collectStyles(<App />));
const styleTags = sheet.getStyleTags();// 注入到head
<head>${styleTags}</head>
emotion示例:
import { renderToString } from 'react-dom/server';
import { extractCritical } from '@emotion/server';const { html, css, ids } = extractCritical(renderToString(<App />)
);// 注入樣式
<head><style data-emotion-css="${ids.join(' ')}">${css}</style>
</head>
3. 傳統CSS文件
處理方式:
- 使用webpack的file-loader處理CSS文件引用
- 在HTML模板中插入link標簽
- 確保文件通過靜態資源中間件可訪問
// webpack配置
{test: /\.css$/,use: [{loader: 'file-loader',options: {name: 'static/css/[name].[hash].css'}}]
}// HTML模板
<link rel="stylesheet" href="/static/css/main.123456.css">
24. 服務端如何處理用戶請求的headers和cookies?
處理Headers
app.get('*', (req, res) => {// 讀取headersconst userAgent = req.headers['user-agent'];const acceptLanguage = req.headers['accept-language'];// 設置響應headersres.set({'X-Custom-Header': 'value','Cache-Control': 'no-cache'});// 根據headers做不同處理if (req.headers['x-mobile-version']) {// 返回移動端特定內容}
});
處理Cookies
const cookieParser = require('cookie-parser');
app.use(cookieParser());app.get('*', (req, res) => {// 讀取cookiesconst authToken = req.cookies.authToken;const userId = req.cookies.userId;// 設置cookiesres.cookie('lastVisit', new Date().toISOString(), {maxAge: 900000,httpOnly: true});// 刪除cookieres.clearCookie('oldCookie');
});
與客戶端共享狀態
// 服務端將cookies注入到全局狀態
const initialState = {auth: {token: req.cookies.authToken}
};const html = `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)};</script>
`;
認證授權處理
// 檢查認證狀態
function checkAuth(req) {const token = req.cookies.token || req.headers.authorization;return verifyToken(token);
}// 受保護路由
app.get('/profile', (req, res) => {if (!checkAuth(req)) {return res.redirect('/login');}// 渲染受保護內容
});
25. 如何在服務端實現301/302重定向?
Express實現方式
// 302臨時重定向
app.get('/old-path', (req, res) => {res.redirect('/new-path');
});// 301永久重定向
app.get('/old-path-permanent', (req, res) => {res.redirect(301, '/new-path');
});// 動態決定重定向狀態碼
app.get('/smart-redirect', (req, res) => {const isPermanent = req.query.permanent === 'true';res.redirect(isPermanent ? 301 : 302, '/target');
});
Koa實現方式
router.get('/old-path', (ctx) => {ctx.redirect('/new-path'); // 默認302
});router.get('/old-path-permanent', (ctx) => {ctx.status = 301;ctx.redirect('/new-path');
});
SSR組件內重定向
React Router示例:
// 服務端路由配置
import { StaticRouter } from 'react-router-dom';app.get('*', (req, res) => {const context = {};const html = ReactDOMServer.renderToString(<StaticRouter location={req.url} context={context}><App /></StaticRouter>);// 檢查是否觸發重定向if (context.url) {return res.redirect(301, context.url);}res.send(html);
});
注意事項
- 301重定向會被瀏覽器緩存,謹慎使用
- 對于SEO敏感頁面使用301
- 在開發環境可以使用302方便測試
- 重定向時考慮保留查詢參數:
res.redirect(`/new-path${req.originalUrl.slice(req.path.length)}`);
26. 如何設計SSR服務的錯誤處理和降級機制?
錯誤處理策略
-
全局錯誤捕獲
// Express中間件 app.use((err, req, res, next) => {console.error('SSR Error:', err);// 根據錯誤類型選擇處理方式if (err.code === 'MODULE_NOT_FOUND') {return res.status(500).send('Server configuration error');}// 默認降級到CSRreturn sendCSRFallback(res); });
-
渲染超時處理
function renderWithTimeout(app, timeout = 3000) {return new Promise((resolve, reject) => {const timer = setTimeout(() => {reject(new Error('SSR Timeout'));}, timeout);try {const html = ReactDOMServer.renderToString(app);clearTimeout(timer);resolve(html);} catch (err) {clearTimeout(timer);reject(err);}}); }
降級機制實現
-
CSR降級方案
function sendCSRFallback(res) {res.send(`<!DOCTYPE html><html><head><title>App</title></head><body><div id="root"></div><script src="/client.bundle.js"></script></body></html>`); }
-
緩存降級方案
- 使用Redis緩存成功渲染的頁面
- 出錯時返回最近一次成功渲染的結果
-
靜態頁面降級
- 為關鍵頁面準備靜態HTML版本
- 出錯時返回靜態版本
監控與報警
-
錯誤分類
- 組件渲染錯誤
- 數據獲取錯誤
- 內存泄漏
- 渲染超時
-
監控指標
const stats = {ssrSuccess: 0,ssrFailures: 0,fallbackToCSR: 0,renderTime: 0 };// 記錄指標 app.use((req, res, next) => {const start = Date.now();res.on('finish', () => {stats.renderTime = Date.now() - start;});next(); });
27. 如何處理動態導入(dynamic import)的組件在服務端的渲染?
解決方案
-
使用@loadable/component
// 組件定義 import loadable from '@loadable/component'; const DynamicComponent = loadable(() => import('./DynamicComponent'));// 服務端處理 import { ChunkExtractor } from '@loadable/server';const statsFile = path.resolve('../dist/loadable-stats.json'); const extractor = new ChunkExtractor({ statsFile });const html = ReactDOMServer.renderToString(extractor.collectChunks(<App />) );const scriptTags = extractor.getScriptTags();
-
React.lazy的SSR適配
// 需要自定義Suspense的SSR支持 function lazy(loader) {let loaded = null;return function LazyComponent(props) {if (loaded) return <loaded.default {...props} />;throw loader().then(mod => {loaded = mod;});}; }// 服務端捕獲Promise const promises = []; const html = ReactDOMServer.renderToString(<Suspense fallback={<div>Loading...</div>}><ErrorBoundary><App /></ErrorBoundary></Suspense> );await Promise.all(promises);
-
Babel插件轉換
- 使用babel-plugin-dynamic-import-node
- 在服務端將動態導入轉換為同步require
數據預取策略
// 組件定義靜態方法
Component.fetchData = async () => {const data = await fetch('/api/data');return data;
};// 服務端渲染時收集數據需求
const dataRequirements = matchRoutes(routes, req.path).map(({ route }) => route.component?.fetchData).filter(Boolean);const data = await Promise.all(dataRequirements.map(fn => fn()));
28. 在Node.js服務器中,如何管理和復用渲染器實例以提升性能?
渲染器池化技術
-
基礎池化實現
class RendererPool {constructor(size = 4) {this.pool = new Array(size).fill(null).map(() => new Renderer());this.queue = [];}acquire() {return new Promise((resolve) => {const renderer = this.pool.pop();if (renderer) return resolve(renderer);this.queue.push(resolve);});}release(renderer) {if (this.queue.length) {const resolve = this.queue.shift();resolve(renderer);} else {this.pool.push(renderer);}} }
-
使用generic-pool
const pool = genericPool.createPool({create: () => createRenderer(),destroy: (renderer) => renderer.cleanup() }, {max: 10,min: 2 });const html = await pool.use(renderer => renderer.renderToString(<App />) );
V8隔離實例
const { NodeVM } = require('vm2');
const vm = new NodeVM({sandbox: {},require: {external: true}
});function createIsolate() {return vm.run(`const React = require('react');const ReactDOMServer = require('react-dom/server');{render: (component) => ReactDOMServer.renderToString(component)}`);
}
緩存策略
- LRU緩存渲染結果
const LRU = require('lru-cache'); const ssrCache = new LRU({max: 100,maxAge: 1000 * 60 * 5 // 5分鐘 });app.get('*', (req, res) => {const cacheKey = req.url;if (ssrCache.has(cacheKey)) {return res.send(ssrCache.get(cacheKey));}renderToString(<App />).then(html => {ssrCache.set(cacheKey, html);res.send(html);}); });
性能優化技巧
-
預熱緩存
// 啟動時預先渲染常用路由 const warmupRoutes = ['/', '/about', '/contact']; Promise.all(warmupRoutes.map(route => {return renderToString(<App location={route} />); }));
-
內存管理
// 定期清理內存 setInterval(() => {if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {rendererPool.clear();gc(); // 需要--expose-gc} }, 60000);
29. 如何實現一個通用的SSR中間件(Express/Koa)?
Express中間件實現
function createSSRMiddleware(options = {}) {const {bundlePath,template,clientStats,cacheEnabled = true} = options;const bundle = require(bundlePath);const cache = new LRU({ max: 100 });return async function ssrMiddleware(req, res, next) {// 跳過非GET請求或特定路徑if (req.method !== 'GET' || req.path.startsWith('/api')) {return next();}// 檢查緩存const cacheKey = req.url;if (cacheEnabled && cache.has(cacheKey)) {return res.send(cache.get(cacheKey));}try {// 渲染組件const { default: App, fetchData } = bundle;const data = fetchData ? await fetchData(req) : {};const html = await renderToString(<App data={data} />);// 應用模板const fullHtml = template.replace('<!--ssr-outlet-->', html).replace('<!--ssr-state-->', `<script>window.__DATA__=${serialize(data)}</script>`);// 設置緩存if (cacheEnabled) {cache.set(cacheKey, fullHtml);}res.send(fullHtml);} catch (err) {// 降級處理if (options.fallback) {res.send(options.fallback);} else {next(err);}}};
}
Koa中間件實現
function koaSSR(options) {return async (ctx, next) => {if (ctx.method !== 'GET') return next();try {const rendered = await renderApp(ctx);ctx.type = 'html';ctx.body = rendered.html;// 處理重定向if (rendered.redirect) {ctx.status = rendered.redirect.status || 302;ctx.redirect(rendered.redirect.url);}} catch (err) {if (options.fallbackToClient) {ctx.type = 'html';ctx.body = options.fallbackToClient;} else {throw err;}}};
}
生產環境特性
-
請求隔離
const vm = new NodeVM({sandbox: {url: req.url,headers: req.headers},require: {external: true,builtin: ['fs', 'path']} });
-
安全處理
// XSS防護 const serializeState = (state) => {return JSON.stringify(state).replace(/</g, '\\u003c'); };// CSP頭 res.setHeader('Content-Security-Policy', "default-src 'self'");
-
性能監控
middleware.use((req, res, next) => {const start = Date.now();res.on('finish', () => {metrics.timing('ssr.render_time', Date.now() - start);});next(); });
30. SSR應用的代碼是如何打包的?(需要為Client和Server分別打包)
Webpack配置方案
-
基礎目錄結構
/configwebpack.client.jswebpack.server.js /srcclient/server/shared/
-
客戶端配置 (webpack.client.js)
module.exports = {target: 'web',entry: './src/client/index.js',output: {path: path.resolve('dist/client'),filename: '[name].[chunkhash].js',publicPath: '/static/'},module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: 'babel-loader'},{test: /\.css$/,use: ['style-loader', 'css-loader']}]},plugins: [new MiniCssExtractPlugin(),new WebpackManifestPlugin()] };
-
服務端配置 (webpack.server.js)
module.exports = {target: 'node',entry: './src/server/index.js',output: {path: path.resolve('dist/server'),filename: 'server.js',libraryTarget: 'commonjs2'},externals: [nodeExternals()],module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: 'babel-loader'},{test: /\.css$/,use: {loader: 'css-loader',options: {onlyLocals: true}}}]} };
高級打包策略
-
代碼分割
// 客戶端配置 optimization: {splitChunks: {chunks: 'all'} }// 動態導入 import(/* webpackChunkName: "lodash" */ 'lodash').then(...)
-
環境變量注入
new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),'process.env.API_URL': JSON.stringify(process.env.API_URL) })
-
服務端外部依賴
// webpack.server.js externals: [nodeExternals({allowlist: [/\.(?!(?:jsx?|json)$).{1,5}$/i]}) ]
構建流程優化
-
并行構建
// package.json {"scripts": {"build": "npm-run-all --parallel build:client build:server","build:client": "webpack --config config/webpack.client.js","build:server": "webpack --config config/webpack.server.js"} }
-
DLL打包
// webpack.dll.js new webpack.DllPlugin({name: '[name]_[hash]',path: path.join(__dirname, 'manifest.json') })
-
構建分析
new BundleAnalyzerPlugin({analyzerMode: 'static',reportFilename: 'report.html' })
四、數據預取與狀態管理(31-40)詳細解答
31. 在服務端如何進行數據預取(Data Prefetching)?
靜態方法模式
// 組件定義靜態數據預取方法
class ProductPage extends React.Component {static async fetchData(params, req) {const product = await api.fetchProduct(params.id);const reviews = await api.fetchReviews(params.id);return { product, reviews };}
}// 服務端使用
async function renderApp(req, res) {const dataRequirements = matchRoutes(routes, req.path).map(({ route, match }) => {return route.component.fetchData ? route.component.fetchData(match.params, req): null;}).filter(Boolean);const prefetchedData = await Promise.all(dataRequirements);
}
路由配置模式
// 路由配置中添加數據預取函數
const routes = [{path: '/products/:id',component: ProductPage,fetchData: ({ id }) => fetchProductData(id)}
];// 服務端匹配路由并預取數據
const matchedRoutes = matchRoutes(routes, req.path);
const dataPromises = matchedRoutes.map(({ route, match }) => {return route.fetchData ? route.fetchData(match.params) : null;
});const prefetchedData = await Promise.all(dataPromises);
高階組件模式
function withDataFetching(fetchFn) {return WrappedComponent => {const ExtendedComponent = (props) => <WrappedComponent {...props} />;ExtendedComponent.fetchData = fetchFn;return ExtendedComponent;};
}// 使用示例
const ProductPageWithData = withDataFetching(({ id }) => fetchProductData(id)
)(ProductPage);
數據預取優化技巧
-
并行請求優化
const fetchAllData = async (params) => {const [product, reviews, related] = await Promise.all([api.fetchProduct(params.id),api.fetchReviews(params.id),api.fetchRelated(params.id)]);return { product, reviews, related }; };
-
請求緩存
const apiCache = new Map();async function cachedFetch(url) {if (apiCache.has(url)) {return apiCache.get(url);}const data = await fetch(url);apiCache.set(url, data);return data; }
-
請求優先級
async function fetchPriorityData() {// 關鍵數據立即請求const critical = await fetchCriticalData();// 次要數據延遲請求const secondary = fetchSecondaryData().catch(() => null);return { critical, secondary }; }
32. 服務端獲取的狀態(State)是如何準確地傳遞到客戶端的?
狀態脫水(Dehydration)與注水(Hydration)
-
基本實現方式
// 服務端脫水 const preloadedState = store.getState(); const serializedState = JSON.stringify(preloadedState);const html = `<script>window.__PRELOADED_STATE__ = ${serializedState};</script> `;// 客戶端注水 const preloadedState = window.__PRELOADED_STATE__; const store = createStore(reducer, preloadedState);
-
安全序列化
function safeSerialize(state) {return JSON.stringify(state).replace(/</g, '\\u003c').replace(/u2028/g, '\\u2028').replace(/u2029/g, '\\u2029'); }
-
Redux實現示例
// 服務端 const store = configureStore(); await store.dispatch(fetchData());const html = `<script>window.__REDUX_STATE__ = ${safeSerialize(store.getState())}</script> `;// 客戶端 const store = configureStore({preloadedState: window.__REDUX_STATE__ });
狀態傳輸優化方案
-
按需傳輸
// 只傳輸必要狀態 const essentialState = {user: store.getState().user,products: store.getState().products.list };
-
壓縮狀態
const lzString = require('lz-string'); const compressedState = lzString.compressToEncodedURIComponent(JSON.stringify(store.getState()) );// 客戶端解壓 const decompressed = lzString.decompressFromEncodedURIComponent(window.__COMPRESSED_STATE__ );
-
差異化傳輸
// 計算客戶端已有狀態與服務端狀態的差異 const diff = diffState(clientState, serverState); res.send(`<script>window.__STATE_DIFF__=${JSON.stringify(diff)}</script>`);
33. 在同構應用中,Redux/Pinia等狀態管理庫的 store應如何創建和初始化?
Redux同構實現
-
store工廠函數
// shared/store/configureStore.js import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers';export default function configureStore(initialState = {}) {return createStore(rootReducer,initialState,applyMiddleware(thunk)); }
-
服務端store創建
// server/createStore.js import configureStore from '../shared/store/configureStore';export default async function createServerStore(req) {const store = configureStore();// 執行數據預取的actionawait store.dispatch(fetchUserData(req.cookies.token));await store.dispatch(fetchInitialData());return store; }
-
客戶端store創建
// client/createStore.js import configureStore from '../shared/store/configureStore';export default function createClientStore() {const preloadedState = window.__PRELOADED_STATE__;delete window.__PRELOADED_STATE__;return configureStore(preloadedState); }
Pinia同構實現
-
store工廠函數
// shared/stores/index.js import { createPinia } from 'pinia';export function createSSRStore() {const pinia = createPinia();return pinia; }
-
服務端初始化
// server/app.js import { createSSRStore } from '../shared/stores'; import { useUserStore } from '../shared/stores/user';export async function createApp() {const pinia = createSSRStore();const userStore = useUserStore(pinia);await userStore.fetchUser(req.cookies.token);return { pinia }; }
-
客戶端初始化
// client/main.js import { createSSRStore } from '../shared/stores';const pinia = createSSRStore();if (window.__PINIA_STATE__) {pinia.state.value = window.__PINIA_STATE__; }app.use(pinia);
關鍵注意事項
-
單例問題
- 服務端每次請求必須創建新的store實例
- 避免store狀態在請求間共享
-
序列化限制
- 確保store狀態可序列化
- 避免在state中存儲函數、循環引用等
-
插件兼容性
- 檢查插件是否支持SSR環境
- 可能需要為服務端和客戶端使用不同插件
34. 在服務端發起API請求時,如何處理API的超時和錯誤?
超時處理方案
-
Promise.race實現超時
function fetchWithTimeout(url, options, timeout = 3000) {return Promise.race([fetch(url, options),new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout))]); }
-
axios超時配置
const instance = axios.create({timeout: 5000,timeoutErrorMessage: 'Request timed out' });
-
全局超時攔截器
axios.interceptors.request.use(config => {config.timeout = config.timeout || 3000;return config; });
錯誤處理策略
-
分級錯誤處理
try {const data = await fetchData(); } catch (error) {if (error.isNetworkError) {// 網絡錯誤處理} else if (error.isTimeout) {// 超時處理} else if (error.statusCode === 404) {// 404處理} else {// 其他錯誤} }
-
錯誤邊界組件
class ErrorBoundary extends React.Component {state = { hasError: false };static getDerivedStateFromError() {return { hasError: true };}render() {if (this.state.hasError) {return this.props.fallback;}return this.props.children;} }
-
API錯誤封裝
class ApiError extends Error {constructor(message, status) {super(message);this.status = status;this.isApiError = true;} }async function fetchApi() {const res = await fetch(url);if (!res.ok) {throw new ApiError(res.statusText, res.status);}return res.json(); }
SSR特定處理
-
渲染降級策略
try {const data = await fetchWithTimeout(apiUrl, {}, 3000);return renderWithData(data); } catch (error) {if (error.isTimeout) {// 超時降級渲染return renderWithoutData();}throw error; }
-
錯誤狀態傳遞
// 服務端將錯誤狀態傳遞到客戶端 const initialState = {error: error.isTimeout ? 'timeout' : null };// 客戶端根據錯誤狀態顯示UI if (store.getState().error === 'timeout') {showTimeoutMessage(); }
35. 如何避免客戶端在注水后重復請求服務端已經獲取過的數據?
數據標記法
-
數據版本控制
// 服務端注入數據版本 res.send(`<script>window.__DATA_VERSION__ = '${dataChecksum}';</script> `);// 客戶端檢查版本 if (window.__DATA_VERSION__ !== currentDataChecksum) {fetchNewData(); }
-
數據時效標記
// 服務端設置數據過期時間 res.send(`<script>window.__DATA_EXPIRES__ = ${Date.now() + 300000}; // 5分鐘后過期</script> `);// 客戶端檢查是否過期 if (Date.now() > window.__DATA_EXPIRES__) {fetchNewData(); }
Redux解決方案
-
數據存在性檢查
// 客戶端組件 useEffect(() => {if (!props.data || props.data.length === 0) {props.fetchData();} }, []);
-
時間戳比對
// Redux action const shouldFetchData = (state) => {return !state.data || Date.now() - state.lastUpdated > CACHE_DURATION; };if (shouldFetchData(store.getState())) {store.dispatch(fetchData()); }
請求去重方案
-
請求ID標記
// 服務端生成請求ID const requestId = generateRequestId(data);// 客戶端檢查ID if (window.__REQUEST_ID__ !== currentRequestId) {refetchData(); }
-
數據指紋比對
function getDataFingerprint(data) {return JSON.stringify(data).length; }if (getDataFingerprint(window.__PRELOADED_DATA__) !== getDataFingerprint(currentData)) {fetchNewData(); }
高級解決方案
-
GraphQL數據跟蹤
// 使用Apollo Client的fetchPolicy const { data } = useQuery(GET_DATA, {fetchPolicy: 'cache-first',nextFetchPolicy: 'cache-first' });
-
SWR/React Query緩存
// 使用SWR的revalidateOnMount選項 useSWR('/api/data', fetcher, {revalidateOnMount: !window.__PRELOADED_DATA__,initialData: window.__PRELOADED_DATA__ });
36. 在SSR中如何處理用戶登錄狀態和認證信息?
認證流程設計
-
Cookie-Based認證流程
// 服務端中間件 function authMiddleware(req, res, next) {const token = req.cookies.authToken;if (token && verifyToken(token)) {req.user = decodeToken(token);return next();}res.status(401).redirect('/login'); }
-
JWT認證流程
// 從Header或Cookie獲取token const token = req.headers.authorization?.split(' ')[1] || req.cookies.jwt;if (!token) {return res.status(401).json({ error: 'Unauthorized' }); }try {req.user = jwt.verify(token, secret);next(); } catch (err) {res.clearCookie('jwt');res.status(401).json({ error: 'Invalid token' }); }
狀態同步方案
-
服務端注入用戶狀態
// 服務端渲染前獲取用戶狀態 const user = await getUserFromToken(req.cookies.token);// 注入到全局狀態 const initialState = {auth: {user,isAuthenticated: !!user} };// 傳遞到客戶端 res.send(`<script>window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)};</script> `);
-
客戶端hydrate檢查
// 客戶端初始化時檢查認證狀態 if (window.__PRELOADED_STATE__?.auth?.user) {store.dispatch({ type: 'LOGIN_SUCCESS', payload: window.__PRELOADED_STATE__.auth.user }); }
安全增強措施
-
HttpOnly Cookie
// 設置安全的cookie res.cookie('token', token, {httpOnly: true,secure: process.env.NODE_ENV === 'production',sameSite: 'strict',maxAge: 1000 * 60 * 60 * 24 // 1天 });
-
CSRF防護
// 生成CSRF token const csrfToken = generateToken();// 傳遞給客戶端 res.cookie('XSRF-TOKEN', csrfToken);// 客戶端請求時帶上token axios.defaults.headers.common['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
37. 如何管理需要認證(Auth)的API請求?
請求攔截方案
-
axios攔截器
// 請求攔截器 axios.interceptors.request.use(config => {const token = store.getState().auth.token;if (token) {config.headers.Authorization = `Bearer ${token}`;}return config; });// 響應攔截器 axios.interceptors.response.use(response => response,error => {if (error.response.status === 401) {store.dispatch(logout());window.location = '/login';}return Promise.reject(error);} );
-
fetch封裝
async function authFetch(url, options = {}) {const token = getAuthToken();const headers = {...options.headers,Authorization: `Bearer ${token}`};const response = await fetch(url, { ...options, headers });if (response.status === 401) {clearAuthToken();throw new Error('Unauthorized');}return response; }
SSR認證處理
-
服務端請求傳遞cookie
// 服務端創建axios實例 const serverAxios = axios.create({baseURL: 'https://api.example.com',headers: {Cookie: `authToken=${req.cookies.authToken}`} });
-
認證狀態同步
// 服務端獲取用戶數據 async function getInitialData(req) {try {const { data } = await serverAxios.get('/user', {headers: {Cookie: `authToken=${req.cookies.authToken}`}});return { user: data };} catch (error) {return { user: null };} }
令牌刷新機制
-
自動刷新令牌
// 響應攔截器處理token刷新 axios.interceptors.response.use(response => response,async error => {const originalRequest = error.config;if (error.response.status === 401 && !originalRequest._retry) {originalRequest._retry = true;const newToken = await refreshToken();store.dispatch(updateToken(newToken));originalRequest.headers.Authorization = `Bearer ${newToken}`;return axios(originalRequest);}return Promise.reject(error);} );
-
服務端令牌刷新
app.post('/refresh-token', (req, res) => {const refreshToken = req.cookies.refreshToken;if (!refreshToken) {return res.status(401).json({ error: 'No refresh token' });}try {const decoded = verifyRefreshToken(refreshToken);const newToken = generateToken(decoded.userId);res.cookie('token', newToken, { httpOnly: true });res.json({ token: newToken });} catch (err) {res.status(401).json({ error: 'Invalid refresh token' });} });
38. 服務端預取的數據量過大,會帶來什么問題?如何解決?
大數據量帶來的問題
-
性能問題
- 增加服務端渲染時間
- 增加內存使用量
- 延長TTFB(Time To First Byte)
-
傳輸問題
- 增加HTML文檔大小
- 消耗更多帶寬
- 移動端加載緩慢
-
安全問題
- 可能暴露敏感數據
- 增加XSS攻擊風險
解決方案
-
數據分頁與懶加載
// 只預取第一頁數據 const initialData = await fetchPaginatedData({ page: 1, limit: 10 });// 客戶端加載更多 const loadMore = () => fetchPaginatedData({ page: 2, limit: 10 });
-
數據精簡
// 只選擇必要字段 const minimalData = rawData.map(item => ({id: item.id,title: item.title,image: item.thumbnail }));
-
按需傳輸
// 根據設備類型決定數據量 const isMobile = req.headers['user-agent'].includes('Mobile'); const dataLimit = isMobile ? 10 : 20;const data = await fetchData({ limit: dataLimit });
-
數據壓縮
const LZString = require('lz-string'); const compressed = LZString.compressToBase64(JSON.stringify(data));// 客戶端解壓 const data = JSON.parse(LZString.decompressFromBase64(window.__DATA__));
-
數據拆分
// 關鍵數據立即傳輸 res.write(`<script>window.__CRITICAL_DATA__ = ${JSON.stringify(criticalData)};</script> `);// 非關鍵數據延遲加載 res.write(`<script defer src="/lazy-data.js"></script> `);
39. 如何實現一個與路由關聯的數據預取方案?
基于路由配置的方案
-
路由配置定義
const routes = [{path: '/',component: HomePage,fetchData: () => fetchHomeData()},{path: '/products/:id',component: ProductPage,fetchData: ({ id }) => fetchProductData(id)} ];
-
服務端數據預取
import { matchRoutes } from 'react-router-dom';async function prefetchData(url) {const matchedRoutes = matchRoutes(routes, url);const dataPromises = matchedRoutes.map(({ route, match }) => {return route.fetchData ? route.fetchData(match.params): Promise.resolve(null);});return Promise.all(dataPromises); }
-
客戶端數據同步
// 使用相同的路由配置 function useRouteData() {const location = useLocation();const matchedRoutes = matchRoutes(routes, location.pathname);useEffect(() => {matchedRoutes.forEach(({ route, match }) => {if (route.fetchData && !isDataLoaded(match)) {route.fetchData(match.params);}});}, [location]); }
動態導入集成
-
路由與組件動態加載
const routes = [{path: '/dashboard',component: lazy(() => import('./Dashboard')),fetchData: () => import('./Dashboard/data').then(m => m.fetchData())} ];
-
服務端處理動態路由
async function loadRouteData(route) {if (typeof route.fetchData === 'function') {return route.fetchData();}if (typeof route.component.fetchData === 'function') {return route.component.fetchData();}return null; }
高級路由數據管理
-
數據依賴樹
// 定義數據依賴關系 const dataDependencies = {'/user/:id': {user: ({ id }) => fetchUser(id),posts: ({ id }) => fetchUserPosts(id),friends: ({ id }) => fetchUserFriends(id)} };// 收集所有數據需求 const dataRequirements = getDataRequirements(path, dataDependencies); const data = await fetchAllData(dataRequirements);
-
數據預取中間件
function createDataPrefetchMiddleware(routes) {return store => next => action => {if (action.type === 'LOCATION_CHANGE') {const matched = matchRoutes(routes, action.payload.location.pathname);matched.forEach(({ route, match }) => {if (route.fetchData) {store.dispatch(route.fetchData(match.params));}});}return next(action);}; }
40. 如何處理多個并行數據請求,并等待它們全部完成后再進行渲染?
Promise.all基礎方案
async function fetchAllData() {const [user, products, notifications] = await Promise.all([fetchUser(),fetchProducts(),fetchNotifications()]);return { user, products, notifications };
}// 服務端使用
const data = await fetchAllData();
const html = renderToString(<App {...data} />);
高級并行控制
-
帶錯誤處理的并行請求
async function fetchAllSafe(promises) {const results = await Promise.all(promises.map(p => p.catch(e => {console.error('Fetch error:', e);return null;})));return results; }
-
分批次并行
async function batchFetch(allRequests, batchSize = 5) {const results = [];for (let i = 0; i < allRequests.length; i += batchSize) {const batch = allRequests.slice(i, i + batchSize);const batchResults = await Promise.all(batch);results.push(...batchResults);}return results; }
React Suspense集成
-
資源預加載
function preloadResources(resources) {const promises = resources.map(resource => {return new Promise((resolve) => {const img = new Image();img.src = resource;img.onload = resolve;});});return Promise.all(promises); }
-
SuspenseList控制
<SuspenseList revealOrder="together"><Suspense fallback={<Spinner />}><UserProfile /></Suspense><Suspense fallback={<Spinner />}><ProductList /></Suspense> </SuspenseList>
性能優化技巧
-
請求優先級
async function fetchPrioritized() {// 關鍵數據立即請求const critical = await fetchCriticalData();// 次要數據并行請求const [secondary1, secondary2] = await Promise.all([fetchSecondary1(),fetchSecondary2()]);return { critical, secondary1, secondary2 }; }
-
請求緩存復用
const requestCache = new Map();async function cachedFetch(key, fetchFn) {if (requestCache.has(key)) {return requestCache.get(key);}const promise = fetchFn();requestCache.set(key, promise);return promise; }
-
請求取消
const controller = new AbortController();Promise.all([fetch('/api1', { signal: controller.signal }),fetch('/api2', { signal: controller.signal }) ]).catch(e => {if (e.name === 'AbortError') {console.log('Requests aborted');} });// 超時取消 setTimeout(() => controller.abort(), 5000);