开源的SSR框架都是怎么实现的?
一、前言
SSR、CSR是每一个前端开发者耳闻熟知的词。
两者本质区别:
- SSR由服务端直接返回首屏内容(html)给前端;
- CSR由服务端返回空根节点(root),浏览器解析JS再填充内容(html);
两者的渲染时间差也差在其中,SSR不需要解析完JS再渲染,等一次请求就行。
那业界优秀的支持SSR的框架都是怎么实现的呢?
本文以Alibaba开源框架ice.js来举例,源码级剖析SSR是如何实现的、与CSR的共性、不同点在哪里。
二、SSR的实现
SSR必备的就是一台“解析SSR脚本”的服务器。
而CSR只需要静态托管即可,我们每次发布后所生成的html文件是固定的,直接托管在站点访问即可(SSG也是如此)。
那 解析SSR脚本 这件事,在ice里主要做了哪些呢?
我们先大概猜想构思下:
- 可能有一个
express/koa服务,用于接收所有的请求,基于请求路由来返回路由组件; - 可能有一些处理
服务端React组件的代码,应该会基于react-dom/server; - 最后会以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);}}
}
核心是做了这些事情:
- 读取网络
response对象; - 处理前端组件 -> html的转换;
- 直接进行http响应或者流式响应;
- 出现异常降级到
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 });}
}
核心做了这几件事:
- 解析请求体、基础配置
const location = getLocation(req.url);const requestContext = getRequestContext(location, serverContext);const appConfig = getAppConfig(app);
- 初始化App上下文+运行时上下文(框架层面设计)
const appContext: AppContext = {appExport: app,routes,appConfig,appData,routesData: null,routesConfig: null,assetsManifest,basename,matches: [],};const runtime = new Runtime(appContext, runtimeOptions);
- hash模式直接返回html,因为不支持
ssr
if (appConfig?.router?.type === 'hash') {return renderDocument({ matches: [], renderOptions });}
- 路由匹配,支持默认404
const matches = matchRoutes(routes, location, serverOnlyBasename || basename);if (!matches.length) {return render404();}
- 执行
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核心的最后一步,触达到用户响应。
核心做了这几件事情:
- 将
doRender所初始化、准备的runTime、AppData、AppContext等全局上下文,全部聚合在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();
- 准备根节点App,包括最基本的React严格模式、全局异常捕获,提供路由能力。
const documentContext = {main: <Appaction={Action.Pop}location={location}navigator={staticNavigator}staticRouteWrappers={RouteWrappers}AppRouter={AppRouter}/>,
};
- 组件整颗html树
const element = (<AppDataProvider value={appData}><AppRuntimeProvider><AppContextProvider value={appContext}><DocumentContextProvider value={documentContext}><Document pagePath={routePath} /></DocumentContextProvider></AppContextProvider></AppRuntimeProvider></AppDataProvider>
);
- 流式渲染(基于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的原理。
