Next系统学习(二)

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

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

React + Node.js SSR实现步骤:

  1. 项目结构搭建

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

    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);
  3. 客户端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'));
  4. 共享组件

    javascript 复制代码
    // 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. 服务端入口

    javascript 复制代码
    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. 客户端入口

    javascript 复制代码
    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结构

    javascript 复制代码
    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. 处理资源路径

    javascript 复制代码
    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)

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

    javascript 复制代码
    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

    javascript 复制代码
    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. 手动管理

    javascript 复制代码
    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

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

    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文件

处理方式:

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

注意事项

  1. 301重定向会被浏览器缓存,谨慎使用

  2. 对于SEO敏感页面使用301

  3. 在开发环境可以使用302方便测试

  4. 重定向时考虑保留查询参数:

    javascript 复制代码
    res.redirect(`/new-path${req.originalUrl.slice(req.path.length)}`);

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

错误处理策略

  1. 全局错误捕获

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

    javascript 复制代码
    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降级方案

    javascript 复制代码
    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. 监控指标

    javascript 复制代码
    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

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

渲染器池化技术

  1. 基础池化实现

    javascript 复制代码
    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

    javascript 复制代码
    const 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)
    }
  `);
}

缓存策略

  1. LRU缓存渲染结果

    javascript 复制代码
    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. 预热缓存

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

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

生产环境特性

  1. 请求隔离

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

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

    javascript 复制代码
    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. 基础目录结构

    复制代码
    /config
      webpack.client.js
      webpack.server.js
    /src
      client/
      server/
      shared/
  2. 客户端配置 (webpack.client.js)

    javascript 复制代码
    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)

    javascript 复制代码
    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. 代码分割

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

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

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

构建流程优化

  1. 并行构建

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

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

    javascript 复制代码
    new 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);

数据预取优化技巧

  1. 并行请求优化

    javascript 复制代码
    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. 请求缓存

    javascript 复制代码
    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. 请求优先级

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

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

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

  1. 基本实现方式

    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);
  2. 安全序列化

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

状态传输优化方案

  1. 按需传输

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

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

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

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

Redux同构实现

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

  1. store工厂函数

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

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

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

关键注意事项

  1. 单例问题

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

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

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

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

超时处理方案

  1. Promise.race实现超时

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

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

    javascript 复制代码
    axios.interceptors.request.use(config => {
      config.timeout = config.timeout || 3000;
      return config;
    });

错误处理策略

  1. 分级错误处理

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

    javascript 复制代码
    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错误封装

    javascript 复制代码
    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. 渲染降级策略

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

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

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

数据标记法

  1. 数据版本控制

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

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

Redux解决方案

  1. 数据存在性检查

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

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

请求去重方案

  1. 请求ID标记

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

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

高级解决方案

  1. GraphQL数据跟踪

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

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

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

认证流程设计

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

状态同步方案

  1. 服务端注入用户状态

    javascript 复制代码
    // 服务端渲染前获取用户状态
    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检查

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

安全增强措施

  1. HttpOnly Cookie

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

请求拦截方案

  1. 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);
      }
    );
  2. fetch封装

    javascript 复制代码
    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

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

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

令牌刷新机制

  1. 自动刷新令牌

    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);
      }
    );
  2. 服务端令牌刷新

    javascript 复制代码
    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. 数据分页与懒加载

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

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

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

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

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

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

基于路由配置的方案

  1. 路由配置定义

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

    javascript 复制代码
    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. 客户端数据同步

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

动态导入集成

  1. 路由与组件动态加载

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

    javascript 复制代码
    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. 数据依赖树

    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);
  2. 数据预取中间件

    javascript 复制代码
    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基础方案

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

高级并行控制

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

    javascript 复制代码
    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. 分批次并行

    javascript 复制代码
    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. 资源预加载

    javascript 复制代码
    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控制

    javascript 复制代码
    <SuspenseList revealOrder="together">
      <Suspense fallback={<Spinner />}>
        <UserProfile />
      </Suspense>
      <Suspense fallback={<Spinner />}>
        <ProductList />
      </Suspense>
    </SuspenseList>

性能优化技巧

  1. 请求优先级

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

    javascript 复制代码
    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. 请求取消

    javascript 复制代码
    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);
相关推荐
BillKu2 小时前
vue3 中 npm install mammoth 与 npm install --save mammoth 的主要区别说明
前端·npm·node.js
织_网2 小时前
Electron 核心模块速查表
javascript·electron·策略模式
Ankle2 小时前
vue3 父子组件v-model传值方法总结
前端·vue.js
EndingCoder2 小时前
Electron 原生模块集成:使用 N-API
javascript·electron·node.js·桌面端
Liquidliang3 小时前
用Claude Code构建AI创意工作流:连接nano banana与veo3
前端·aigc
半花3 小时前
【Vue】defineProps直接和withDefaults设置默认值区别
前端·vue.js
游九尘3 小时前
服务器都是用的iis, 前端部署后报跨域,不是用同一个服务器 是前端项目的服务器做Nginx转发,还是后端项目的服务器做Nginx转发?
服务器·前端·nginx
携欢3 小时前
PortSwigger靶场之DOM XSS in jQuery selector sink using a hashchange event通关秘籍
前端·jquery·xss
Apifox3 小时前
如何让 Apifox 发布的在线文档具备更好的调试体验?
前端·后端·测试