Next系统学习(二)

SSR生命周期与实现详细解答

19. 如果不使用框架,如何从零用React/Vue+Node.js实现一个简单的SSR应用?

React + Node.js SSR实现步骤:

  1. 项目结构搭建

    /project/client - 客户端代码/server - 服务端代码/shared - 共享代码
    
  2. 服务端基础设置

    // 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);
    
  3. 客户端hydrate

    // client/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from '../shared/App';ReactDOM.hydrate(<App />, document.getElementById('root'));
    
  4. 共享组件

    // shared/App.js
    import React from 'react';const App = () => (<div><h1>Hello SSR</h1></div>
    );export default App;
    
  5. Webpack配置

    • 客户端配置:target: ‘web’
    • 服务端配置:target: ‘node’

Vue + Node.js SSR实现步骤:

  1. 服务端入口

    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>`);});
    });
    
  2. 客户端入口

    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字符串。主要作用包括:

  1. 初始渲染:在服务器端生成完整的HTML结构,包含组件初始状态的渲染结果
  2. SEO优化:搜索引擎可以直接抓取已渲染的HTML内容
  3. 首屏性能:用户能立即看到已渲染的内容,无需等待JS加载执行
  4. hydration基础:为后续客户端hydrate提供标记点

工作原理:

  • 递归遍历React组件树
  • 生成对应的HTML字符串
  • 不包含事件处理等交互逻辑
  • 保留data-reactid等属性用于客户端hydrate

特点:

  • 同步操作,会阻塞事件循环直到渲染完成
  • 不支持组件生命周期方法(如componentDidMount)
  • 不支持refs
  • 生成的HTML不包含客户端交互逻辑

与renderToStaticMarkup()的区别:

  • renderToString会添加额外的React内部使用的DOM属性
  • renderToStaticMarkup生成更干净的HTML,但不支持hydrate

21. 服务端如何构建一个完整的HTML响应?

构建完整HTML响应的关键步骤:

  1. 基本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>
    `;
    
  2. 动态注入内容

    • 使用模板引擎(如EJS、Pug)
    • 或字符串拼接方式插入变量
  3. 处理资源路径

    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>`;
    
  4. 状态脱水(State Dehydration)

    const preloadedState = serializeState(store.getState());
    const stateScript = `<script>window.__PRELOADED_STATE__ = ${preloadedState}</script>`;
    
  5. 完整示例

    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解决方案:

  1. 使用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()}
    `;
    
  2. 手动管理

    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解决方案:

  1. 使用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>
    `;
    
  2. 动态路由匹配

    // 根据路由配置匹配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文件

处理方式:

  1. 使用webpack的file-loader处理CSS文件引用
  2. 在HTML模板中插入link标签
  3. 确保文件通过静态资源中间件可访问
// 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);
});

注意事项

  1. 301重定向会被浏览器缓存,谨慎使用
  2. 对于SEO敏感页面使用301
  3. 在开发环境可以使用302方便测试
  4. 重定向时考虑保留查询参数:
    res.redirect(`/new-path${req.originalUrl.slice(req.path.length)}`);
    

26. 如何设计SSR服务的错误处理和降级机制?

错误处理策略

  1. 全局错误捕获

    // 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);
    });
    
  2. 渲染超时处理

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

降级机制实现

  1. 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>`);
    }
    
  2. 缓存降级方案

    • 使用Redis缓存成功渲染的页面
    • 出错时返回最近一次成功渲染的结果
  3. 静态页面降级

    • 为关键页面准备静态HTML版本
    • 出错时返回静态版本

监控与报警

  1. 错误分类

    • 组件渲染错误
    • 数据获取错误
    • 内存泄漏
    • 渲染超时
  2. 监控指标

    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)的组件在服务端的渲染?

解决方案

  1. 使用@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();
    
  2. 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);
    
  3. 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服务器中,如何管理和复用渲染器实例以提升性能?

渲染器池化技术

  1. 基础池化实现

    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);}}
    }
    
  2. 使用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)}`);
}

缓存策略

  1. 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);});
    });
    

性能优化技巧

  1. 预热缓存

    // 启动时预先渲染常用路由
    const warmupRoutes = ['/', '/about', '/contact'];
    Promise.all(warmupRoutes.map(route => {return renderToString(<App location={route} />);
    }));
    
  2. 内存管理

    // 定期清理内存
    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;}}};
}

生产环境特性

  1. 请求隔离

    const vm = new NodeVM({sandbox: {url: req.url,headers: req.headers},require: {external: true,builtin: ['fs', 'path']}
    });
    
  2. 安全处理

    // XSS防护
    const serializeState = (state) => {return JSON.stringify(state).replace(/</g, '\\u003c');
    };// CSP头
    res.setHeader('Content-Security-Policy', "default-src 'self'");
    
  3. 性能监控

    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配置方案

  1. 基础目录结构

    /configwebpack.client.jswebpack.server.js
    /srcclient/server/shared/
    
  2. 客户端配置 (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()]
    };
    
  3. 服务端配置 (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}}}]}
    };
    

高级打包策略

  1. 代码分割

    // 客户端配置
    optimization: {splitChunks: {chunks: 'all'}
    }// 动态导入
    import(/* webpackChunkName: "lodash" */ 'lodash').then(...)
    
  2. 环境变量注入

    new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),'process.env.API_URL': JSON.stringify(process.env.API_URL)
    })
    
  3. 服务端外部依赖

    // webpack.server.js
    externals: [nodeExternals({allowlist: [/\.(?!(?:jsx?|json)$).{1,5}$/i]})
    ]
    

构建流程优化

  1. 并行构建

    // 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"}
    }
    
  2. DLL打包

    // webpack.dll.js
    new webpack.DllPlugin({name: '[name]_[hash]',path: path.join(__dirname, 'manifest.json')
    })
    
  3. 构建分析

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

数据预取优化技巧

  1. 并行请求优化

    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 };
    };
    
  2. 请求缓存

    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;
    }
    
  3. 请求优先级

    async function fetchPriorityData() {// 关键数据立即请求const critical = await fetchCriticalData();// 次要数据延迟请求const secondary = fetchSecondaryData().catch(() => null);return { critical, secondary };
    }
    

32. 服务端获取的状态(State)是如何准确地传递到客户端的?

状态脱水(Dehydration)与注水(Hydration)

  1. 基本实现方式

    // 服务端脱水
    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);
    
  2. 安全序列化

    function safeSerialize(state) {return JSON.stringify(state).replace(/</g, '\\u003c').replace(/u2028/g, '\\u2028').replace(/u2029/g, '\\u2029');
    }
    
  3. 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__
    });
    

状态传输优化方案

  1. 按需传输

    // 只传输必要状态
    const essentialState = {user: store.getState().user,products: store.getState().products.list
    };
    
  2. 压缩状态

    const lzString = require('lz-string');
    const compressedState = lzString.compressToEncodedURIComponent(JSON.stringify(store.getState())
    );// 客户端解压
    const decompressed = lzString.decompressFromEncodedURIComponent(window.__COMPRESSED_STATE__
    );
    
  3. 差异化传输

    // 计算客户端已有状态与服务端状态的差异
    const diff = diffState(clientState, serverState);
    res.send(`<script>window.__STATE_DIFF__=${JSON.stringify(diff)}</script>`);
    

33. 在同构应用中,Redux/Pinia等状态管理库的 store应如何创建和初始化?

Redux同构实现

  1. 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));
    }
    
  2. 服务端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;
    }
    
  3. 客户端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同构实现

  1. store工厂函数

    // shared/stores/index.js
    import { createPinia } from 'pinia';export function createSSRStore() {const pinia = createPinia();return pinia;
    }
    
  2. 服务端初始化

    // 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 };
    }
    
  3. 客户端初始化

    // client/main.js
    import { createSSRStore } from '../shared/stores';const pinia = createSSRStore();if (window.__PINIA_STATE__) {pinia.state.value = window.__PINIA_STATE__;
    }app.use(pinia);
    

关键注意事项

  1. 单例问题

    • 服务端每次请求必须创建新的store实例
    • 避免store状态在请求间共享
  2. 序列化限制

    • 确保store状态可序列化
    • 避免在state中存储函数、循环引用等
  3. 插件兼容性

    • 检查插件是否支持SSR环境
    • 可能需要为服务端和客户端使用不同插件

34. 在服务端发起API请求时,如何处理API的超时和错误?

超时处理方案

  1. Promise.race实现超时

    function fetchWithTimeout(url, options, timeout = 3000) {return Promise.race([fetch(url, options),new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout))]);
    }
    
  2. axios超时配置

    const instance = axios.create({timeout: 5000,timeoutErrorMessage: 'Request timed out'
    });
    
  3. 全局超时拦截器

    axios.interceptors.request.use(config => {config.timeout = config.timeout || 3000;return config;
    });
    

错误处理策略

  1. 分级错误处理

    try {const data = await fetchData();
    } catch (error) {if (error.isNetworkError) {// 网络错误处理} else if (error.isTimeout) {// 超时处理} else if (error.statusCode === 404) {// 404处理} else {// 其他错误}
    }
    
  2. 错误边界组件

    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;}
    }
    
  3. 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特定处理

  1. 渲染降级策略

    try {const data = await fetchWithTimeout(apiUrl, {}, 3000);return renderWithData(data);
    } catch (error) {if (error.isTimeout) {// 超时降级渲染return renderWithoutData();}throw error;
    }
    
  2. 错误状态传递

    // 服务端将错误状态传递到客户端
    const initialState = {error: error.isTimeout ? 'timeout' : null
    };// 客户端根据错误状态显示UI
    if (store.getState().error === 'timeout') {showTimeoutMessage();
    }
    

35. 如何避免客户端在注水后重复请求服务端已经获取过的数据?

数据标记法

  1. 数据版本控制

    // 服务端注入数据版本
    res.send(`<script>window.__DATA_VERSION__ = '${dataChecksum}';</script>
    `);// 客户端检查版本
    if (window.__DATA_VERSION__ !== currentDataChecksum) {fetchNewData();
    }
    
  2. 数据时效标记

    // 服务端设置数据过期时间
    res.send(`<script>window.__DATA_EXPIRES__ = ${Date.now() + 300000}; // 5分钟后过期</script>
    `);// 客户端检查是否过期
    if (Date.now() > window.__DATA_EXPIRES__) {fetchNewData();
    }
    

Redux解决方案

  1. 数据存在性检查

    // 客户端组件
    useEffect(() => {if (!props.data || props.data.length === 0) {props.fetchData();}
    }, []);
    
  2. 时间戳比对

    // Redux action
    const shouldFetchData = (state) => {return !state.data || Date.now() - state.lastUpdated > CACHE_DURATION;
    };if (shouldFetchData(store.getState())) {store.dispatch(fetchData());
    }
    

请求去重方案

  1. 请求ID标记

    // 服务端生成请求ID
    const requestId = generateRequestId(data);// 客户端检查ID
    if (window.__REQUEST_ID__ !== currentRequestId) {refetchData();
    }
    
  2. 数据指纹比对

    function getDataFingerprint(data) {return JSON.stringify(data).length;
    }if (getDataFingerprint(window.__PRELOADED_DATA__) !== getDataFingerprint(currentData)) {fetchNewData();
    }
    

高级解决方案

  1. GraphQL数据跟踪

    // 使用Apollo Client的fetchPolicy
    const { data } = useQuery(GET_DATA, {fetchPolicy: 'cache-first',nextFetchPolicy: 'cache-first'
    });
    
  2. SWR/React Query缓存

    // 使用SWR的revalidateOnMount选项
    useSWR('/api/data', fetcher, {revalidateOnMount: !window.__PRELOADED_DATA__,initialData: window.__PRELOADED_DATA__
    });
    

36. 在SSR中如何处理用户登录状态和认证信息?

认证流程设计

  1. 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');
    }
    
  2. 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' });
    }
    

状态同步方案

  1. 服务端注入用户状态

    // 服务端渲染前获取用户状态
    const user = await getUserFromToken(req.cookies.token);// 注入到全局状态
    const initialState = {auth: {user,isAuthenticated: !!user}
    };// 传递到客户端
    res.send(`<script>window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)};</script>
    `);
    
  2. 客户端hydrate检查

    // 客户端初始化时检查认证状态
    if (window.__PRELOADED_STATE__?.auth?.user) {store.dispatch({ type: 'LOGIN_SUCCESS', payload: window.__PRELOADED_STATE__.auth.user });
    }
    

安全增强措施

  1. HttpOnly Cookie

    // 设置安全的cookie
    res.cookie('token', token, {httpOnly: true,secure: process.env.NODE_ENV === 'production',sameSite: 'strict',maxAge: 1000 * 60 * 60 * 24 // 1天
    });
    
  2. 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请求?

请求拦截方案

  1. 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);}
    );
    
  2. 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认证处理

  1. 服务端请求传递cookie

    // 服务端创建axios实例
    const serverAxios = axios.create({baseURL: 'https://api.example.com',headers: {Cookie: `authToken=${req.cookies.authToken}`}
    });
    
  2. 认证状态同步

    // 服务端获取用户数据
    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 };}
    }
    

令牌刷新机制

  1. 自动刷新令牌

    // 响应拦截器处理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);}
    );
    
  2. 服务端令牌刷新

    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. 服务端预取的数据量过大,会带来什么问题?如何解决?

大数据量带来的问题

  1. 性能问题

    • 增加服务端渲染时间
    • 增加内存使用量
    • 延长TTFB(Time To First Byte)
  2. 传输问题

    • 增加HTML文档大小
    • 消耗更多带宽
    • 移动端加载缓慢
  3. 安全问题

    • 可能暴露敏感数据
    • 增加XSS攻击风险

解决方案

  1. 数据分页与懒加载

    // 只预取第一页数据
    const initialData = await fetchPaginatedData({ page: 1, limit: 10 });// 客户端加载更多
    const loadMore = () => fetchPaginatedData({ page: 2, limit: 10 });
    
  2. 数据精简

    // 只选择必要字段
    const minimalData = rawData.map(item => ({id: item.id,title: item.title,image: item.thumbnail
    }));
    
  3. 按需传输

    // 根据设备类型决定数据量
    const isMobile = req.headers['user-agent'].includes('Mobile');
    const dataLimit = isMobile ? 10 : 20;const data = await fetchData({ limit: dataLimit });
    
  4. 数据压缩

    const LZString = require('lz-string');
    const compressed = LZString.compressToBase64(JSON.stringify(data));// 客户端解压
    const data = JSON.parse(LZString.decompressFromBase64(window.__DATA__));
    
  5. 数据拆分

    // 关键数据立即传输
    res.write(`<script>window.__CRITICAL_DATA__ = ${JSON.stringify(criticalData)};</script>
    `);// 非关键数据延迟加载
    res.write(`<script defer src="/lazy-data.js"></script>
    `);
    

39. 如何实现一个与路由关联的数据预取方案?

基于路由配置的方案

  1. 路由配置定义

    const routes = [{path: '/',component: HomePage,fetchData: () => fetchHomeData()},{path: '/products/:id',component: ProductPage,fetchData: ({ id }) => fetchProductData(id)}
    ];
    
  2. 服务端数据预取

    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);
    }
    
  3. 客户端数据同步

    // 使用相同的路由配置
    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]);
    }
    

动态导入集成

  1. 路由与组件动态加载

    const routes = [{path: '/dashboard',component: lazy(() => import('./Dashboard')),fetchData: () => import('./Dashboard/data').then(m => m.fetchData())}
    ];
    
  2. 服务端处理动态路由

    async function loadRouteData(route) {if (typeof route.fetchData === 'function') {return route.fetchData();}if (typeof route.component.fetchData === 'function') {return route.component.fetchData();}return null;
    }
    

高级路由数据管理

  1. 数据依赖树

    // 定义数据依赖关系
    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);
    
  2. 数据预取中间件

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

高级并行控制

  1. 带错误处理的并行请求

    async function fetchAllSafe(promises) {const results = await Promise.all(promises.map(p => p.catch(e => {console.error('Fetch error:', e);return null;})));return results;
    }
    
  2. 分批次并行

    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集成

  1. 资源预加载

    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);
    }
    
  2. SuspenseList控制

    <SuspenseList revealOrder="together"><Suspense fallback={<Spinner />}><UserProfile /></Suspense><Suspense fallback={<Spinner />}><ProductList /></Suspense>
    </SuspenseList>
    

性能优化技巧

  1. 请求优先级

    async function fetchPrioritized() {// 关键数据立即请求const critical = await fetchCriticalData();// 次要数据并行请求const [secondary1, secondary2] = await Promise.all([fetchSecondary1(),fetchSecondary2()]);return { critical, secondary1, secondary2 };
    }
    
  2. 请求缓存复用

    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;
    }
    
  3. 请求取消

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/96426.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/96426.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

零代码入侵:Kubernetes 部署时自动注入 kube-system UID 到 .NET 9 环境变量

在现代化 .net9 应用部署阶段&#xff0c;零代码入侵模式&#xff0c;自动获取 kubernetes 命名空间 kube-system 的 UID&#xff0c;并其作为变量配置到应用。 以下是几种实现方式&#xff1a; 方法一&#xff1a;使用 InitContainer Downward API 您可以通过 Kubernetes 的 …

基于Redis设计一个高可用的缓存

本文为您介绍&#xff0c;如何逐步设计一个基于Redis的高可用缓存。 目录 业务背景 步骤一&#xff1a;写一个最简单的缓存设计 存在的问题&#xff1a;大量冷数据占据Redis内存 解决思路&#xff1a;让缓存自主释放 步骤二&#xff1a;为缓存设置超时时间 存在的问题&a…

从原理到实践:LVS+Keepalived构建高可用负载均衡集群

从原理到实践&#xff1a;LVSKeepalived构建高可用负载均衡集群 文章目录从原理到实践&#xff1a;LVSKeepalived构建高可用负载均衡集群一、为什么需要LVSKeepalived&#xff1f;二、核心原理&#xff1a;Keepalived与VRRP协议1. VRRP的核心思想2. Keepalived的三大功能三、LV…

iOS混淆工具实战 在线教育直播类 App 的课程与互动安全防护

近年来&#xff0c;在线教育直播类 App 已成为学生与培训机构的重要工具。无论是 K12 教育、职业培训&#xff0c;还是兴趣学习&#xff0c;App 中承载的课程视频、题库与互动逻辑都是极高价值的内容资产。 然而&#xff0c;教育直播应用同样面临多重安全风险&#xff1a;课程视…

第2节-过滤表中的行-BETWEEN

摘要: 在本教程中&#xff0c;您将学习如何在 WHERE 子句中使用 PostgreSQL 的 BETWEEN 运算符来检查某个值是否在两个值之间。 PostgreSQL BETWEEN 运算符 BETWEEN运算符是一种比较运算符&#xff0c;如果某个值介于两个值之间&#xff0c;则返回true。 以下是 BETWEEN 运算符…

Windows 11 手动下载安装配置 uv、配置国内源

Windows 11 手动下载安装配置 uv、配置国内源 本文对应的讲解视频链接&#xff1a;https://www.bilibili.com/video/BV1WnYTzZEpW 文章目录Windows 11 手动下载安装配置 uv、配置国内源1. 下载、安装、配置 uv2. 参考信息重要声明&#xff1a; uv 的安装有很多种方式&#xff…

平板热点频繁断连?三步彻底解决

平板反复断开热点连接是一个非常常见且令人烦恼的问题。这通常不是单一原因造成的&#xff0c;而是多种因素叠加的结果。 我们可以从热点发射设备&#xff08;手机等&#xff09;、平板本身、以及环境因素三个方面来排查和解决。 一、 热点发射端&#xff08;通常是手机&#x…

Qt文件操作的学习(三)

一、实现简易文本编辑器 主要用到带菜单栏的窗口&#xff0c;而非单一窗口。QT已经写好相关操作&#xff0c;就不在重新造轮子了功能设计&#xff1a;新建文本文档&#xff0c;打开文件&#xff0c;保存文件&#xff0c;另存为 这次不同于之前直接可以在控件上面右击槽了&…

ArcGIS学习-20 实战-县域水文分析

水文分析任务提取区域内水流方向、汇流累积量、河网、流域、子流域前置操作环境更改加载数据检查投影坐标系河网分析洼地填充限制默认为空&#xff0c;认为所有洼地都是需要填充的&#xff0c;这里更正一下Fill_DEM需要加上后缀.tif流向分析得到流量分析得到这里的黑色代表非河…

本地 Docker 环境 Solr 配置 SSL 证书

一、简介 在本地开发环境中为 Solr 配置 SSL 证书,是提升开发与测试一致性的关键步骤。尤其是在涉及安全传输需求的场景中,本地环境的 HTTPS 配置能有效避免因环境差异导致的问题。本文将详细介绍如何利用 Docker 容器,快速为 Solr 服务配置自签名 SSL 证书,实现本地 HTTP…

MacOS 运行CosyVoice

CosyVoic主要特点&#xff1a;1、支持中文、英文、上海话、天津话、四川话等方言。语音非常自然。2、支持3秒语音零样本克隆&#xff0c;效果非常好。3、克隆时间比较长&#xff08;取决于GPU性能&#xff0c;使用H20以满足低延迟输出&#xff09;&#xff0c;L4 克隆默认文本需…

我不是挂王-用python实现燕双鹰小游戏3

在前两个版本的更新后,越来越多内容,操作和运行也不方便,优化第三版本窗口可视化界面 本次版本更新使得可读性和可操作性大幅度增加,前面2版本可分别参考 我不是挂王-用python实现燕双鹰小游戏 和 我不是挂王-用python实现燕双鹰小游戏2 一.燕双鹰窗口可视化(燕双鹰3.0) 新燕双…

装饰(Decorator)模式可以在不修改对象外观和功能的情况下添加或者删除对象功能

试题&#xff08;35&#xff09;、&#xff08;36&#xff09;某系统中的文本显示类&#xff08;TextView&#xff09;和图片显示类&#xff08;PictureView&#xff09;都继承了组件类&#xff08;Component&#xff09;&#xff0c;分别显示文本和图片内容&#xff0c;现需要…

深度学习基础概念【持续更新】

1. 梯度消失如果网络中某一层的激活函数&#xff08;如 sigmoid 或 tanh&#xff09;在输入较大的情况下有很小的梯度&#xff08;比如接近零&#xff09;&#xff0c;那么当这些小的梯度通过多层反向传播时&#xff0c;它们会逐渐变得更小。这意味着在深层网络的前面几层&…

上下文工程:AI应用成功的关键架构与实践指南

在AI应用开发中&#xff0c;模型能力只决定性能上限&#xff0c;而上下文质量决定性能下限——上下文工程正是确保AI系统理解用户意图、生成准确响应的核心工程技术&#xff0c;已成为区分普通AI应用与卓越AI应用的关键因素。一、上下文工程&#xff1a;AI应用的新核心竞争力 1…

数据传输优化-异步不阻塞处理增强首屏体验

背景&#xff1a;主 project 页面中会将视频存储到云端后获得 ID &#xff0c;然后用 ID 调用 后端API POST到数据库后拿到挂载页面URL&#xff0c;接着传入视频分享组件&#xff08;由于视频分享子组件的目标是分享视频挂载页面&#xff0c;所以前置步骤不能少&#xff09;con…

【芯片设计-信号完整性 SI 学习 1.0 -- SI 介绍】

文章目录一、SoC 设计验证阶段的 SI 测试主要工作举例二、芯片 Bringup 阶段的 SI 测试主要工作举例三、SI-PI 联合仿真主要内容举例四、整体总结一、SoC 设计验证阶段的 SI 测试 在 前硅阶段&#xff08;pre-silicon&#xff09;&#xff0c;设计团队需要确保 SoC 与外设接口…

C语言链表设计及应用

链表链表节点设计链表项目链表中的传址调用检查申请空间链表尾插链表头插链表尾部删除链表头部删除链表的查找指定位置之前插入指定位置之后插入数据删除指定位置&#xff08;节点&#xff09;数据删除指定位置&#xff08;节点&#xff09;之后的数据链表的销毁前面学习了顺序…

使用 YAML 自动化 Azure DevOps 管道

1. 在 Azure DevOps 中设置 YAML 管道 开始之前,您需要拥有一个 Azure DevOps 帐户和一个 git 仓库。 要创建 YAML 管道, 1. 导航至 Azure DevOps → 选择您的项目 2. 前往“管道”→ 点击“新建管道” 3. 选择您的仓库(Azure Repos、GitHub 等) 4. 选择“Starter Pipelin…

基于Spring Boot的幼儿园管理系统

基于Spring Boot的幼儿园管理系统 源码获取&#xff1a;https://mbd.pub/o/bread/YZWXlZtsbQ 引言 在数字化转型的浪潮中&#xff0c;教育行业的信息化建设显得尤为重要。幼儿园作为基础教育的重要环节&#xff0c;其管理系统的现代化水平直接关系到教育质量和运营效率。本文…