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);