当前位置: 首页 > news >正文

开源的SSR框架都是怎么实现的?

一、前言

SSRCSR是每一个前端开发者耳闻熟知的词。

两者本质区别:

  • SSR由服务端直接返回首屏内容(html)给前端;
  • CSR由服务端返回空根节点(root),浏览器解析JS再填充内容(html);

两者的渲染时间差也差在其中,SSR不需要解析完JS再渲染,等一次请求就行。

那业界优秀的支持SSR的框架都是怎么实现的呢?

本文以Alibaba开源框架ice.js来举例,源码级剖析SSR是如何实现的、与CSR的共性、不同点在哪里。

二、SSR的实现

SSR必备的就是一台“解析SSR脚本”的服务器。

而CSR只需要静态托管即可,我们每次发布后所生成的html文件是固定的,直接托管在站点访问即可(SSG也是如此)。

解析SSR脚本 这件事,在ice里主要做了哪些呢?

我们先大概猜想构思下:

  1. 可能有一个express/koa服务,用于接收所有的请求,基于请求路由来返回路由组件;
  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/steam)都找到了。

再看下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 CSRif (!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 });}
}

核心做了这几件事:

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

OK,这里实际上还处理框架层面的渲染前准备工作,做了一些框架上的运行时、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: <Appaction={Action.Pop}location={location}navigator={staticNavigator}staticRouteWrappers={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应用根节点,用户可以基于透传的数据在业务组件中消费。
  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();
  1. 准备根节点App,包括最基本的React严格模式、全局异常捕获,提供路由能力。
const documentContext = {main: <Appaction={Action.Pop}location={location}navigator={staticNavigator}staticRouteWrappers={RouteWrappers}AppRouter={AppRouter}/>,
};
  1. 组件整颗html树
const element = (<AppDataProvider value={appData}><AppRuntimeProvider><AppContextProvider value={appContext}><DocumentContextProvider value={documentContext}><Document pagePath={routePath} /></DocumentContextProvider></AppContextProvider></AppRuntimeProvider></AppDataProvider>
);
  1. 流式渲染(基于react/dom renderToPipeableStream 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,一次完整的服务端渲染结束。

/*** 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是如何基于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 finishconst { 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模式),在dev server加了一层,如果是ssr,则动态引入服务端渲染函数,直接走渲染链路(即上节链路),否则就走常规webpack构建链路。

四、结尾

希望文章让你有所帮助和收获,让你更加了解SSR的原理。

http://www.dtcms.com/a/520395.html

相关文章:

  • RLVR训练多模态文档解析模型-olmOCR 2技术方案(模型、数据和代码均开源)
  • AI 领域热门方向或代表性技术/模型
  • MySQL 体系结构、SQL 执行与设计范式
  • 个人网站如何搭建国家企业信用信息网官网
  • MySQL学习之SQL语法与操作
  • “麻烦您了”英语怎么说?
  • 临时上线没有回滚方案会怎样
  • 哪个网站做高仿衣服中小学网站建设建议
  • Linux 中的 DNS 工作原理(二):各级 DNS 缓存
  • vip影视网站如何做app建设电子商务网站的预期收益
  • 从 DeepWalk 到 Node2Vec:如何让图学习“更聪明”?
  • leetcode合并有序链表
  • 知识图谱遇上大语言模型:天作之合还是理想泡影?
  • Kafka入门:基础架构讲解,安装与使用
  • 深圳seo网站推广报价wordpress导航栏的文件在哪
  • 电手术刀VS神经调音师:解密电刺激技术差异
  • lance + duckdb 替代 parquet + pandas
  • CHIA考试报告手册
  • Linux操作系统学习之---线程互斥(互斥锁)
  • 【物联网控制体系项目实战】—— 整体架构流程与 WS 实现
  • dedecms网站后台模板做汽车网站费用
  • 做网站就上房山华网天下大型网站如何开发
  • 从「能用」到「可靠」:深入探讨C++异常安全
  • 如何让AI更好地理解中文PDF中的复杂格式?
  • Mount Image Pro,在取证安全的环境中挂载和访问镜像文件内容
  • 四元数(Quaternion)之Eigen::Quaternion使用详解(5)
  • 太平洋建设集团有限公司网站wordpress标签扩展
  • 二级域名解析网站天津效果图制作公司
  • Linux iptables:四表五链 + 实用配置
  • Ceph 简介