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
英文地址,請注明出處:http://en.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;其管理系統的現代化水平直接關系到教育質量和運營效率。本文…