一、前言
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
直接映入眼帘,非常言简意赅,就是处理服务端渲染 -> 响应的函数。
tsx
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
是如何处理的。
tsx
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 });
}
}
核心做了这几件事:
- 解析请求体、基础配置
ts
const location = getLocation(req.url);
const requestContext = getRequestContext(location, serverContext);
const appConfig = getAppConfig(app);
- 初始化App上下文+运行时上下文(框架层面设计)
ts
const appContext: AppContext = {
appExport: app,
routes,
appConfig,
appData,
routesData: null,
routesConfig: null,
assetsManifest,
basename,
matches: [],
};
const runtime = new Runtime(appContext, runtimeOptions);
- hash模式直接返回html,因为不支持
ssr
ts
if (appConfig?.router?.type === 'hash') {
return renderDocument({ matches: [], renderOptions });
}
- 路由匹配,支持默认404
ts
const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
if (!matches.length) {
return render404();
}
- 执行
SSR真实渲染
ts
return await renderServerEntry({
runtime,
matches,
location,
renderOptions,
});
OK,这里实际上还处理框架层面的渲染前准备工作 ,做了一些框架上的运行时、App全局处理记录,可能是用于提供框架对外透出的生命周期钩子函数,与SSR
本身关系不大,先不管。
我们看下renderServerEntry
是如何执行渲染的?
tsx
/**
* 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
核心的最后一步,触达到用户响应。
核心做了这几件事情:
- 将
doRender
所初始化、准备的runTime
、AppData
、AppContext
等全局上下文,全部聚合在React
应用根节点,用户可以基于透传的数据在业务组件中消费。
tsx
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严格模式、全局异常捕获,提供路由能力。
tsx
const documentContext = {
main: <App
action={Action.Pop}
location={location}
navigator={staticNavigator}
static
RouteWrappers={RouteWrappers}
AppRouter={AppRouter}
/>,
};
- 组件整颗html树
tsx
const element = (
<AppDataProvider value={appData}>
<AppRuntimeProvider>
<AppContextProvider value={appContext}>
<DocumentContextProvider value={documentContext}>
<Document pagePath={routePath} />
</DocumentContextProvider>
</AppContextProvider>
</AppRuntimeProvider>
</AppDataProvider>
);
- 流式渲染(基于react/dom renderToPipeableStream API)
tsx
const pipe = renderToNodeStream(element, false);
renderToNodeStream源码:
tsx
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
,一次完整的服务端渲染结束。
ts
/**
* 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
函数:
tsx
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模式),在dev server
加了一层,如果是ssr
,则动态引入服务端渲染函数,直接走渲染链路(即上节链路),否则就走常规webpack
构建链路。
四、结尾
希望文章让你有所帮助和收获,让你更加了解SSR
的原理。