SSR生命周期与实现详细解答
19. 如果不使用框架,如何从零用React/Vue+Node.js实现一个简单的SSR应用?
React + Node.js SSR实现步骤:
-
项目结构搭建
/project /client - 客户端代码 /server - 服务端代码 /shared - 共享代码
-
服务端基础设置
javascript// 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
javascript// client/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from '../shared/App'; ReactDOM.hydrate(<App />, document.getElementById('root'));
-
共享组件
javascript// 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实现步骤:
-
服务端入口
javascriptconst 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> `); }); });
-
客户端入口
javascriptimport 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结构
javascriptconst 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)
- 或字符串拼接方式插入变量
-
处理资源路径
javascriptconst 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)
javascriptconst preloadedState = serializeState(store.getState()); const stateScript = `<script>window.__PRELOADED_STATE__ = ${preloadedState}</script>`;
-
完整示例
javascriptfunction 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
javascriptimport { 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()} `;
-
手动管理
javascriptconst 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
javascript// 组件中 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> `;
-
动态路由匹配
javascript// 根据路由配置匹配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
- 提取类名映射关系
javascript
// 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示例:
javascript
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示例:
javascript
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标签
- 确保文件通过静态资源中间件可访问
javascript
// 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
javascript
app.get('*', (req, res) => {
// 读取headers
const userAgent = req.headers['user-agent'];
const acceptLanguage = req.headers['accept-language'];
// 设置响应headers
res.set({
'X-Custom-Header': 'value',
'Cache-Control': 'no-cache'
});
// 根据headers做不同处理
if (req.headers['x-mobile-version']) {
// 返回移动端特定内容
}
});
处理Cookies
javascript
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('*', (req, res) => {
// 读取cookies
const authToken = req.cookies.authToken;
const userId = req.cookies.userId;
// 设置cookies
res.cookie('lastVisit', new Date().toISOString(), {
maxAge: 900000,
httpOnly: true
});
// 删除cookie
res.clearCookie('oldCookie');
});
与客户端共享状态
javascript
// 服务端将cookies注入到全局状态
const initialState = {
auth: {
token: req.cookies.authToken
}
};
const html = `
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)};
</script>
`;
认证授权处理
javascript
// 检查认证状态
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实现方式
javascript
// 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实现方式
javascript
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示例:
javascript
// 服务端路由配置
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方便测试
-
重定向时考虑保留查询参数:
javascriptres.redirect(`/new-path${req.originalUrl.slice(req.path.length)}`);
26. 如何设计SSR服务的错误处理和降级机制?
错误处理策略
-
全局错误捕获
javascript// 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'); } // 默认降级到CSR return sendCSRFallback(res); });
-
渲染超时处理
javascriptfunction 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降级方案
javascriptfunction 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版本
- 出错时返回静态版本
监控与报警
-
错误分类
- 组件渲染错误
- 数据获取错误
- 内存泄漏
- 渲染超时
-
监控指标
javascriptconst 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
javascript// 组件定义 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适配
javascript// 需要自定义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
数据预取策略
javascript
// 组件定义静态方法
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服务器中,如何管理和复用渲染器实例以提升性能?
渲染器池化技术
-
基础池化实现
javascriptclass 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
javascriptconst pool = genericPool.createPool({ create: () => createRenderer(), destroy: (renderer) => renderer.cleanup() }, { max: 10, min: 2 }); const html = await pool.use(renderer => renderer.renderToString(<App />) );
V8隔离实例
javascript
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缓存渲染结果
javascriptconst 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); }); });
性能优化技巧
-
预热缓存
javascript// 启动时预先渲染常用路由 const warmupRoutes = ['/', '/about', '/contact']; Promise.all(warmupRoutes.map(route => { return renderToString(<App location={route} />); }));
-
内存管理
javascript// 定期清理内存 setInterval(() => { if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) { rendererPool.clear(); gc(); // 需要--expose-gc } }, 60000);
29. 如何实现一个通用的SSR中间件(Express/Koa)?
Express中间件实现
javascript
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中间件实现
javascript
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;
}
}
};
}
生产环境特性
-
请求隔离
javascriptconst vm = new NodeVM({ sandbox: { url: req.url, headers: req.headers }, require: { external: true, builtin: ['fs', 'path'] } });
-
安全处理
javascript// XSS防护 const serializeState = (state) => { return JSON.stringify(state).replace(/</g, '\\u003c'); }; // CSP头 res.setHeader('Content-Security-Policy', "default-src 'self'");
-
性能监控
javascriptmiddleware.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配置方案
-
基础目录结构
/config webpack.client.js webpack.server.js /src client/ server/ shared/
-
客户端配置 (webpack.client.js)
javascriptmodule.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)
javascriptmodule.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 } } } ] } };
高级打包策略
-
代码分割
javascript// 客户端配置 optimization: { splitChunks: { chunks: 'all' } } // 动态导入 import(/* webpackChunkName: "lodash" */ 'lodash').then(...)
-
环境变量注入
javascriptnew webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.API_URL': JSON.stringify(process.env.API_URL) })
-
服务端外部依赖
javascript// webpack.server.js externals: [ nodeExternals({ allowlist: [/\.(?!(?:jsx?|json)$).{1,5}$/i] }) ]
构建流程优化
-
并行构建
json// 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打包
javascript// webpack.dll.js new webpack.DllPlugin({ name: '[name]_[hash]', path: path.join(__dirname, 'manifest.json') })
-
构建分析
javascriptnew BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: 'report.html' })
四、数据预取与状态管理(31-40)详细解答
31. 在服务端如何进行数据预取(Data Prefetching)?
静态方法模式
javascript
// 组件定义静态数据预取方法
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);
}
路由配置模式
javascript
// 路由配置中添加数据预取函数
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);
高阶组件模式
javascript
function withDataFetching(fetchFn) {
return WrappedComponent => {
const ExtendedComponent = (props) => <WrappedComponent {...props} />;
ExtendedComponent.fetchData = fetchFn;
return ExtendedComponent;
};
}
// 使用示例
const ProductPageWithData = withDataFetching(
({ id }) => fetchProductData(id)
)(ProductPage);
数据预取优化技巧
-
并行请求优化
javascriptconst 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 }; };
-
请求缓存
javascriptconst 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; }
-
请求优先级
javascriptasync function fetchPriorityData() { // 关键数据立即请求 const critical = await fetchCriticalData(); // 次要数据延迟请求 const secondary = fetchSecondaryData().catch(() => null); return { critical, secondary }; }
32. 服务端获取的状态(State)是如何准确地传递到客户端的?
状态脱水(Dehydration)与注水(Hydration)
-
基本实现方式
javascript// 服务端脱水 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);
-
安全序列化
javascriptfunction safeSerialize(state) { return JSON.stringify(state) .replace(/</g, '\\u003c') .replace(/u2028/g, '\\u2028') .replace(/u2029/g, '\\u2029'); }
-
Redux实现示例
javascript// 服务端 const store = configureStore(); await store.dispatch(fetchData()); const html = ` <script> window.__REDUX_STATE__ = ${safeSerialize(store.getState())} </script> `; // 客户端 const store = configureStore({ preloadedState: window.__REDUX_STATE__ });
状态传输优化方案
-
按需传输
javascript// 只传输必要状态 const essentialState = { user: store.getState().user, products: store.getState().products.list };
-
压缩状态
javascriptconst lzString = require('lz-string'); const compressedState = lzString.compressToEncodedURIComponent( JSON.stringify(store.getState()) ); // 客户端解压 const decompressed = lzString.decompressFromEncodedURIComponent( window.__COMPRESSED_STATE__ );
-
差异化传输
javascript// 计算客户端已有状态与服务端状态的差异 const diff = diffState(clientState, serverState); res.send(`<script>window.__STATE_DIFF__=${JSON.stringify(diff)}</script>`);
33. 在同构应用中,Redux/Pinia等状态管理库的 store应如何创建和初始化?
Redux同构实现
-
store工厂函数
javascript// 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创建
javascript// server/createStore.js import configureStore from '../shared/store/configureStore'; export default async function createServerStore(req) { const store = configureStore(); // 执行数据预取的action await store.dispatch(fetchUserData(req.cookies.token)); await store.dispatch(fetchInitialData()); return store; }
-
客户端store创建
javascript// 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工厂函数
javascript// shared/stores/index.js import { createPinia } from 'pinia'; export function createSSRStore() { const pinia = createPinia(); return pinia; }
-
服务端初始化
javascript// 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 }; }
-
客户端初始化
javascript// 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实现超时
javascriptfunction fetchWithTimeout(url, options, timeout = 3000) { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout) ) ]); }
-
axios超时配置
javascriptconst instance = axios.create({ timeout: 5000, timeoutErrorMessage: 'Request timed out' });
-
全局超时拦截器
javascriptaxios.interceptors.request.use(config => { config.timeout = config.timeout || 3000; return config; });
错误处理策略
-
分级错误处理
javascripttry { const data = await fetchData(); } catch (error) { if (error.isNetworkError) { // 网络错误处理 } else if (error.isTimeout) { // 超时处理 } else if (error.statusCode === 404) { // 404处理 } else { // 其他错误 } }
-
错误边界组件
javascriptclass 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错误封装
javascriptclass 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特定处理
-
渲染降级策略
javascripttry { const data = await fetchWithTimeout(apiUrl, {}, 3000); return renderWithData(data); } catch (error) { if (error.isTimeout) { // 超时降级渲染 return renderWithoutData(); } throw error; }
-
错误状态传递
javascript// 服务端将错误状态传递到客户端 const initialState = { error: error.isTimeout ? 'timeout' : null }; // 客户端根据错误状态显示UI if (store.getState().error === 'timeout') { showTimeoutMessage(); }
35. 如何避免客户端在注水后重复请求服务端已经获取过的数据?
数据标记法
-
数据版本控制
javascript// 服务端注入数据版本 res.send(` <script> window.__DATA_VERSION__ = '${dataChecksum}'; </script> `); // 客户端检查版本 if (window.__DATA_VERSION__ !== currentDataChecksum) { fetchNewData(); }
-
数据时效标记
javascript// 服务端设置数据过期时间 res.send(` <script> window.__DATA_EXPIRES__ = ${Date.now() + 300000}; // 5分钟后过期 </script> `); // 客户端检查是否过期 if (Date.now() > window.__DATA_EXPIRES__) { fetchNewData(); }
Redux解决方案
-
数据存在性检查
javascript// 客户端组件 useEffect(() => { if (!props.data || props.data.length === 0) { props.fetchData(); } }, []);
-
时间戳比对
javascript// Redux action const shouldFetchData = (state) => { return !state.data || Date.now() - state.lastUpdated > CACHE_DURATION; }; if (shouldFetchData(store.getState())) { store.dispatch(fetchData()); }
请求去重方案
-
请求ID标记
javascript// 服务端生成请求ID const requestId = generateRequestId(data); // 客户端检查ID if (window.__REQUEST_ID__ !== currentRequestId) { refetchData(); }
-
数据指纹比对
javascriptfunction getDataFingerprint(data) { return JSON.stringify(data).length; } if (getDataFingerprint(window.__PRELOADED_DATA__) !== getDataFingerprint(currentData)) { fetchNewData(); }
高级解决方案
-
GraphQL数据跟踪
javascript// 使用Apollo Client的fetchPolicy const { data } = useQuery(GET_DATA, { fetchPolicy: 'cache-first', nextFetchPolicy: 'cache-first' });
-
SWR/React Query缓存
javascript// 使用SWR的revalidateOnMount选项 useSWR('/api/data', fetcher, { revalidateOnMount: !window.__PRELOADED_DATA__, initialData: window.__PRELOADED_DATA__ });
36. 在SSR中如何处理用户登录状态和认证信息?
认证流程设计
-
Cookie-Based认证流程
javascript// 服务端中间件 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认证流程
javascript// 从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' }); }
状态同步方案
-
服务端注入用户状态
javascript// 服务端渲染前获取用户状态 const user = await getUserFromToken(req.cookies.token); // 注入到全局状态 const initialState = { auth: { user, isAuthenticated: !!user } }; // 传递到客户端 res.send(` <script> window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)}; </script> `);
-
客户端hydrate检查
javascript// 客户端初始化时检查认证状态 if (window.__PRELOADED_STATE__?.auth?.user) { store.dispatch({ type: 'LOGIN_SUCCESS', payload: window.__PRELOADED_STATE__.auth.user }); }
安全增强措施
-
HttpOnly Cookie
javascript// 设置安全的cookie res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 1000 * 60 * 60 * 24 // 1天 });
-
CSRF防护
javascript// 生成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拦截器
javascript// 请求拦截器 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封装
javascriptasync 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
javascript// 服务端创建axios实例 const serverAxios = axios.create({ baseURL: 'https://api.example.com', headers: { Cookie: `authToken=${req.cookies.authToken}` } });
-
认证状态同步
javascript// 服务端获取用户数据 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 }; } }
令牌刷新机制
-
自动刷新令牌
javascript// 响应拦截器处理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); } );
-
服务端令牌刷新
javascriptapp.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攻击风险
解决方案
-
数据分页与懒加载
javascript// 只预取第一页数据 const initialData = await fetchPaginatedData({ page: 1, limit: 10 }); // 客户端加载更多 const loadMore = () => fetchPaginatedData({ page: 2, limit: 10 });
-
数据精简
javascript// 只选择必要字段 const minimalData = rawData.map(item => ({ id: item.id, title: item.title, image: item.thumbnail }));
-
按需传输
javascript// 根据设备类型决定数据量 const isMobile = req.headers['user-agent'].includes('Mobile'); const dataLimit = isMobile ? 10 : 20; const data = await fetchData({ limit: dataLimit });
-
数据压缩
javascriptconst LZString = require('lz-string'); const compressed = LZString.compressToBase64(JSON.stringify(data)); // 客户端解压 const data = JSON.parse(LZString.decompressFromBase64(window.__DATA__));
-
数据拆分
javascript// 关键数据立即传输 res.write(` <script> window.__CRITICAL_DATA__ = ${JSON.stringify(criticalData)}; </script> `); // 非关键数据延迟加载 res.write(` <script defer src="/lazy-data.js"></script> `);
39. 如何实现一个与路由关联的数据预取方案?
基于路由配置的方案
-
路由配置定义
javascriptconst routes = [ { path: '/', component: HomePage, fetchData: () => fetchHomeData() }, { path: '/products/:id', component: ProductPage, fetchData: ({ id }) => fetchProductData(id) } ];
-
服务端数据预取
javascriptimport { 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); }
-
客户端数据同步
javascript// 使用相同的路由配置 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]); }
动态导入集成
-
路由与组件动态加载
javascriptconst routes = [ { path: '/dashboard', component: lazy(() => import('./Dashboard')), fetchData: () => import('./Dashboard/data').then(m => m.fetchData()) } ];
-
服务端处理动态路由
javascriptasync function loadRouteData(route) { if (typeof route.fetchData === 'function') { return route.fetchData(); } if (typeof route.component.fetchData === 'function') { return route.component.fetchData(); } return null; }
高级路由数据管理
-
数据依赖树
javascript// 定义数据依赖关系 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);
-
数据预取中间件
javascriptfunction 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基础方案
javascript
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} />);
高级并行控制
-
带错误处理的并行请求
javascriptasync function fetchAllSafe(promises) { const results = await Promise.all( promises.map(p => p.catch(e => { console.error('Fetch error:', e); return null; })) ); return results; }
-
分批次并行
javascriptasync 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集成
-
资源预加载
javascriptfunction 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控制
javascript<SuspenseList revealOrder="together"> <Suspense fallback={<Spinner />}> <UserProfile /> </Suspense> <Suspense fallback={<Spinner />}> <ProductList /> </Suspense> </SuspenseList>
性能优化技巧
-
请求优先级
javascriptasync function fetchPrioritized() { // 关键数据立即请求 const critical = await fetchCriticalData(); // 次要数据并行请求 const [secondary1, secondary2] = await Promise.all([ fetchSecondary1(), fetchSecondary2() ]); return { critical, secondary1, secondary2 }; }
-
请求缓存复用
javascriptconst 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; }
-
请求取消
javascriptconst 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);