找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

505

积分

0

好友

65

主题
发表于 21 小时前 | 查看: 3| 回复: 0

SSR与CSR,是前端开发者再熟悉不过的两种渲染模式。它们的核心区别在于:SSR由服务端直接返回包含首屏内容的HTML;而CSR则由服务端返回一个空根节点,再由浏览器解析并执行JavaScript来填充内容。因此,两者的主要渲染时间差就产生在这里,SSR无需等待JS解析便可直接呈现内容。

那么,业界优秀的支持SSR的框架究竟是如何实现的呢?本文将以Alibaba的开源框架ice.js为例,进行源码级的剖析,探究SSR的实现机制、以及与CSR的共性和差异。

SSR的实现

实现SSR必备的是一台能够解析SSR脚本的服务器。相比之下,CSR只需进行静态资源托管即可,每次构建生成的HTML文件是固定的,直接由站点托管访问(SSG模式也是如此)。

那么,解析SSR脚本这件事,在ice.js框架里主要做了什么呢?我们可以先进行一个大致的构思:

  1. 可能有一个ExpressKoa服务,用于接收所有请求,并根据请求路由返回对应的路由组件。
  2. 应该会有一些处理服务端React组件的代码,很可能会基于react-dom/server
  3. 最终会以HTTP响应或流式渲染的形式将结果返回给前端。

基于这些猜想,我们直接深入源码。这些能力本质上属于运行时(runtime),因此在 /runtime/runServerApp.tsx 文件中,我找到了专门处理服务端渲染的核心部分:

图片

renderToResponse 这个函数名非常直观,其作用就是处理服务端渲染并生成响应。

export async function renderToResponse(requestContext: ServerContext, renderOptions: RenderOptions) {
  const { res } = requestContext;
  const result = await doRender(requestContext, renderOptions);
  const { value } = result;
  if (typeof value === 'string') {
    sendResult(res, result);
  } else {
    const { pipe, fallback } = value;
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    try {
      await pipeToResponse(res, pipe);
    } catch (error) {
      if (renderOptions.disableFallback) {
        throw error;
      }
      console.error('PiperToResponse error, downgrade to CSR.', error);
      // downgrade to CSR.
      const result = await fallback();
      sendResult(res, result);
    }
  }
}

该函数核心完成了以下几件事:

  1. 读取网络响应(Response)对象。
  2. 处理前端组件到HTML的转换。
  3. 直接进行HTTP响应或流式响应。
  4. 出现异常时,降级为CSR渲染,返回空节点。

至此,我们的多个猜想都得到了验证:网络请求、响应(HTTP/Stream)都已找到。接下来看看 doRender 函数是如何处理的。

async function doRender(serverContext: ServerContext, renderOptions: RenderOptions): Promise<RenderResult> {
  const { req } = serverContext;
  const {
    app,
    basename,
    serverOnlyBasename,
    routes,
    documentOnly,
    disableFallback,
    assetsManifest,
    runtimeModules,
    renderMode,
    runtimeOptions,
  } = renderOptions;
  const location = getLocation(req.url);
  const requestContext = getRequestContext(location, serverContext);
  const appConfig = getAppConfig(app);
  let appData: any;
  const appContext: AppContext = {
    appExport: app,
    routes,
    appConfig,
    appData,
    routesData: null,
    routesConfig: null,
    assetsManifest,
    basename,
    matches: [],
  };
  const runtime = new Runtime(appContext, runtimeOptions);
  runtime.setAppRouter(DefaultAppRouter);

  // Load static module before getAppData.
  if (runtimeModules.statics) {
    await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean));
  }

  // don‘t need to execute getAppData in CSR
  if (!documentOnly) {
    try {
      appData = await getAppData(app, requestContext);
    } catch (err) {
      console.error('Error: get app data error when SSR.', err);
    }
  }

  // HashRouter loads route modules by the CSR.
  if (appConfig?.router?.type === 'hash') {
    return renderDocument({ matches: [], renderOptions });
  }

  const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
  if (!matches.length) {
    return render404();
  }

  const routePath = getCurrentRoutePath(matches);
  if (documentOnly) {
    return renderDocument({ matches, routePath, renderOptions });
  }

  try {
    const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load })));
    const routesData = await loadRoutesData(matches, requestContext, routeModules, renderMode);
    const routesConfig = getRoutesConfig(matches, routesData, routeModules);
    runtime.setAppContext({ ...appContext, routeModules, routesData, routesConfig, routePath, matches, appData });
    if (runtimeModules.commons) {
      await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean));
    }
    return await renderServerEntry({
      runtime,
      matches,
      location,
      renderOptions,
    });
  } catch (err) {
    if (disableFallback) {
      throw err;
    }
    console.error('Warning: render server entry error, downgrade to csr.', err);
    return renderDocument({ matches, routePath, renderOptions, downgrade: true });
  }
}

doRender 函数核心完成了以下工作:

  1. 解析请求与配置
    const location = getLocation(req.url);
    const requestContext = getRequestContext(location, serverContext);
    const appConfig = getAppConfig(app);
  2. 初始化应用与运行时上下文(框架层面设计)
    const appContext: AppContext = {
      appExport: app,
      routes,
      appConfig,
      appData,
      routesData: null,
      routesConfig: null,
      assetsManifest,
      basename,
      matches: [],
    };
    const runtime = new Runtime(appContext, runtimeOptions);
  3. Hash路由模式直接返回HTML(不支持SSR)
    if (appConfig?.router?.type === 'hash') {
      return renderDocument({ matches: [], renderOptions });
    }
  4. 路由匹配,支持默认404处理
    const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
    if (!matches.length) {
      return render404();
    }
  5. 执行SSR真实渲染
    return await renderServerEntry({
      runtime,
      matches,
      location,
      renderOptions,
    });

这里实际上还处理了框架层面的渲染前准备工作,初始化运行时和记录App全局状态,可能用于对外提供生命周期钩子函数,这与SSR核心逻辑关系不大,暂且不论。我们重点看 renderServerEntry 是如何执行渲染的。

/**
 * Render App by SSR.
 */
async function renderServerEntry(
  {
    runtime,
    matches,
    location,
    renderOptions,
  }: RenderServerEntry,
): Promise<RenderResult> {
  const { Document } = renderOptions;
  const appContext = runtime.getAppContext();
  const { appData, routePath } = appContext;
  const staticNavigator = createStaticNavigator();
  const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment;
  const RouteWrappers = runtime.getWrappers();
  const AppRouter = runtime.getAppRouter();
  const documentContext = {
    main: <App
      action={Action.Pop}
      location={location}
      navigator={staticNavigator}
      static
      RouteWrappers={RouteWrappers}
      AppRouter={AppRouter}
    />,
  };
  const element = (
    <AppDataProvider value={appData}>
      <AppRuntimeProvider>
        <AppContextProvider value={appContext}>
          <DocumentContextProvider value={documentContext}>
            <Document pagePath={routePath} />
          </DocumentContextProvider>
        </AppContextProvider>
      </AppRuntimeProvider>
    </AppDataProvider>
  );
  const pipe = renderToNodeStream(element, false);
  const fallback = () => {
    return renderDocument({ matches, routePath, renderOptions, downgrade: true });
  };
  return {
    value: {
      pipe,
      fallback,
    },
  };
}

这个函数是SSR核心链路触及用户响应的最后一步,它主要做了以下几件事:

  1. 聚合全局上下文:将doRender初始化和准备的runtimeAppDataAppContext等全局状态,全部聚合到React应用的根节点Provider中,便于业务组件消费。
    const { Document } = renderOptions;
    const appContext = runtime.getAppContext();
    const { appData, routePath } = appContext;
    const staticNavigator = createStaticNavigator();
    const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment;
    const RouteWrappers = runtime.getWrappers();
    const AppRouter = runtime.getAppRouter();
  2. 准备根节点App:构建包含React严格模式、全局异常捕获和路由能力的应用骨架。
    const documentContext = {
      main: <App
        action={Action.Pop}
        location={location}
        navigator={staticNavigator}
        static
        RouteWrappers={RouteWrappers}
        AppRouter={AppRouter}
      />,
    };
  3. 组装完整的HTML组件树
    const element = (
      <AppDataProvider value={appData}>
        <AppRuntimeProvider>
          <AppContextProvider value={appContext}>
            <DocumentContextProvider value={documentContext}>
              <Document pagePath={routePath} />
            </DocumentContextProvider>
          </AppContextProvider>
        </AppRuntimeProvider>
      </AppDataProvider>
    );
  4. 执行流式渲染(基于 react-dom/serverrenderToPipeableStream API)
    const pipe = renderToNodeStream(element, false);

renderToNodeStream 源码:

import * as Stream from 'stream';
import type * as StreamType from 'stream';
import * as ReactDOMServer from 'react-dom/server';
const { Writable } = Stream;
export type NodeWritablePiper = (
  res: StreamType.Writable,
  next?: (err?: Error) => void
) => void;
export function renderToNodeStream(
  element: React.ReactElement,
  generateStaticHTML: boolean,
): NodeWritablePiper {
  return (res, next) => {
    const { pipe } = ReactDOMServer.renderToPipeableStream(
      element,
      {
        onShellReady() {
          if (!generateStaticHTML) {
            pipe(res);
          }
        },
        onAllReady() {
          if (generateStaticHTML) {
            pipe(res);
          }
          next();
        },
        onError(error: Error) {
          next(error);
        },
      },
    );
  };
}

最终,流程回到入口函数 renderToResponse,一次完整的服务端渲染就此完成。这个运行在Node.js环境下的服务端渲染流程清晰展示了如何将React组件树转换为HTML流并发送给客户端。

/**
 * Render and send the result to ServerResponse.
 */
export async function renderToResponse(requestContext: ServerContext, renderOptions: RenderOptions) {
  const { res } = requestContext;
  const result = await doRender(requestContext, renderOptions);
  const { value } = result;
  if (typeof value === 'string') {
    sendResult(res, result);
  } else {
    const { pipe, fallback } = value;
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    try {
      await pipeToResponse(res, pipe);
    } catch (error) {
      if (renderOptions.disableFallback) {
        throw error;
      }
      console.error('PiperToResponse error, downgrade to CSR.', error);
      // downgrade to CSR.
      const result = await fallback();
      sendResult(res, result);
    }
  }
}

开发阶段如何实现?

另一个巧妙之处在于开发阶段。我们都知道SSR依赖于服务端环境,那么在开发阶段,ice.js是如何基于 webpack dev server 来实现SSR的呢?

定位到 /ice/src/commands 目录下的文件,这里包含了框架的命令行逻辑。找到 start 命令相关的实现:

图片

一眼就能发现其中的奥秘:核心是借助了 dev server 的中间件机制,拦截请求并直接触发渲染。我们来细看 createRenderMiddleware 这个函数:

export default function createRenderMiddleware(options: Options): Middleware {
  const {
    documentOnly,
    renderMode,
    serverCompileTask,
    routeManifestPath,
    getAppConfig,
    taskConfig,
    userConfig,
  } = options;
  const middleware: ExpressRequestHandler = async function (req, res, next) {
    const routes = JSON.parse(fse.readFileSync(routeManifestPath, 'utf-8'));
    const appConfig = (await getAppConfig()).default;
    if (appConfig?.router?.type === 'hash') {
      warnOnHashRouterEnabled(userConfig);
    }
    const basename = getRouterBasename(taskConfig, appConfig);
    const matches = matchRoutes(routes, req.path, basename);
    // When documentOnly is true, it means that the app is CSR and it should return the html.
    if (matches.length || documentOnly) {
      // Wait for the server compilation to finish
      const { serverEntry, error } = await serverCompileTask.get();
      if (error) {
        consola.error('Server compile error in render middleware.');
        return;
      }
      let serverModule;
      try {
        delete require.cache[serverEntry];
        serverModule = await dynamicImport(serverEntry, true);
      } catch (err) {
        // make error clearly, notice typeof err === 'string'
        consola.error(`import ${serverEntry} error: ${err}`);
        return;
      }
      const requestContext: ServerContext = {
        req,
        res,
      };
      serverModule.renderToResponse(requestContext, {
        renderMode,
        documentOnly,
      });
    } else {
      next();
    }
  };
  return {
    name: 'server-render',
    middleware,
  };
}

其核心逻辑与服务端主渲染函数并无本质区别。巧妙之处在于,它基于 appConfig 配置(判断是否为SSR模式),在 webpack dev server 的中间件链上加了一层拦截。如果是SSR模式,则动态引入服务端渲染函数,直接走完整的SSR渲染链路(即上一节分析的链路);否则,请求就继续走常规的前端框架静态资源构建和CSR链路。这种设计实现了开发环境下SSR与CSR的无缝切换和高效热更新。

总结

通过以上对ice.js框架SSR实现机制的源码级剖析,我们可以清晰地看到,一个现代SSR框架的核心在于:在服务端构建完整的React应用上下文与组件树,并利用 react-dom/server 的能力将其渲染为HTML。同时,框架需要精巧地处理路由匹配、数据获取、流式响应以及开发时的构建集成。理解这些原理,有助于我们在实践中更好地驾驭SSR技术,优化应用性能与体验。




上一篇:数据治理Agent核心架构详解:AI智能体的技术实现与落地实战指南
下一篇:Windows x64 PE文件异常表深度解析:SEH机制与逆向分析实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-8 22:56 , Processed in 1.091602 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表