Scalar Client-Side Rendering 实战:用 CDN 零依赖渲染 API Reference 静态 HTML
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
本文围绕 Scalar 开源仓库中packages/client-side-rendering包展开,讲解如何通过renderApiReference一行调用,把 OpenAPI/AsyncAPI 规范渲染成可直接下发的静态 HTML 页面。全文覆盖安装、API 签名、ESM/UMD 双 bundle 选择、CSP nonce 安全策略、函数型配置的序列化原理,并结合仓库源码与测试用例给出可复现的实战依据。读完你将能在任意后端框架或静态站点中快速接入 Scalar API Reference,且不引入任何服务端渲染依赖。
什么是 Client-Side Rendering 包
@scalar/client-side-rendering是 Scalar 仓库中负责"客户端渲染"的最小封装包。它的核心思想是:在服务端只生成一段携带配置的 HTML 字符串,真正的 API Reference 界面由浏览器加载 CDN 上的脚本后渲染出来。
这一点在包源码的注释中写得很明确:
"Render the Scalar API Reference as a complete HTML document using the CDN. Generates static HTML that loads the @scalar/api-reference standalone bundle from a CDN and renders client-side. No server-side dependencies required."(见 src/html-rendering.ts)
也就是说,服务端不需要安装 Vue、不需要打包@scalar/api-reference前端代码,只需要返回一个完整的 HTML 文档即可。该包因此成为仓库内多个框架集成(NestJS、Next.js、Fastify、Hono、Express、SvelteKit、Astro、Docusaurus、Starlight 等)共用的底层渲染工具,例如 NestJS 集成就在 integrations/nestjs/src/nestJSApiReference.ts 中直接调用renderApiReference生成响应 HTML。
安装
包通过 npm 分发,包名为@scalar/client-side-rendering:
npm install @scalar/client-side-rendering安装后需要 Node.js 22 及以上版本(见 package.json 中的engines字段)。该包为纯 ESM 模块,生产依赖仅有@scalar/schemas、@scalar/types、@scalar/validation三个工作区包(见 package.json),测试用例 test/few-dependencies.test.ts 专门断言了这一"零额外运行时依赖"的特性。
基础用法:一次调用生成完整 HTML
核心入口是renderApiReference函数,最简单的用法如下(对应 README 中的示例):
import { renderApiReference } from '@scalar/client-side-rendering' const html = renderApiReference({ pageTitle: 'My API Reference', config: { url: 'https://registry.scalar.com/@scalar/apis/galaxy?format=json', }, })返回的html是一个完整的 HTML 文档字符串,可直接作为 HTTP 响应体返回,或写入静态文件。函数完整签名如下(见 src/html-rendering.ts):
renderApiReference( options: { /** API Reference 配置,类型为 AnyApiReferenceConfiguration */ config: AnyApiReferenceConfiguration /** 页面标题,默认 "Scalar API Reference" */ pageTitle?: string /** CDN URL,默认 jsDelivr */ cdn?: string /** CSP nonce,用于严格 CSP 策略 */ nonce?: string /** 加载哪种构建产物:ESM(默认)还是经典 UMD */ bundle?: string | boolean }, customTheme = '', ): string生成的 HTML 结构为标准的<head>+<body>:
<head>中包含<title>(默认值为Scalar API Reference,且pageTitle会经过 HTML 转义,防止 XSS)、<meta charset>、<meta viewport>,以及按需生成的<style>标签;<body>中只包含一个挂载点<div id="app"></div>和一段初始化脚本(见 src/html-rendering.ts)。
测试用例 src/html-rendering.test.ts 验证了默认输出包含<!doctype html>、<title>Scalar API Reference</title>、<script type="module">、<div id="app"></div>以及createApiReference('#app'等关键片段。
配置合并与优先级
renderApiReference内部会调用getConfiguration对配置做归一化处理(见 src/html-rendering.ts):
- 若
content是函数,会先执行它,把返回值作为实际内容; - 若同时提供了
content和url,url优先,content会被移除——因为从 URL 拉取规范时内联内容不再有意义。测试 src/html-rendering.test.ts 专门验证了这一行为。
自定义主题与自定义 CSS
renderApiReference的第二个参数customTheme用于注入自定义主题 CSS;同时config.customCss也支持注入自定义样式。两者都会被拼进<style type="text/css">标签中(见 src/html-rendering.ts):
config.customCss始终注入;customTheme仅在config.theme未设置时注入——即显式设置了内置主题(如theme: 'kepler')时,自定义主题会被跳过,避免冲突。
测试 src/html-rendering.test.ts 验证了"设置了theme属性时不再注入customTheme"这一规则。
选择构建产物:ESM 与 UMD
生成 HTML 时,脚本标签的加载方式决定了页面首屏性能与兼容性。@scalar/client-side-rendering支持两种构建:
默认:现代 code-split ESM 构建
默认情况下,生成的 HTML 通过<script type="module">加载代码分割的 ESM 构建.../@scalar/api-reference/esm.js。由于是代码分割的,浏览器只会按需下载渲染当前页面所需的懒加载 chunk,而不是整个单体 UMD 包,因此更少的 JavaScript 会阻塞首次渲染。
默认 ESM CDN 地址定义在源码常量中(见 src/html-rendering.ts):
export const DEFAULT_ESM_CDN = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference/esm.js'生成的脚本形如:
<script type="module"> import { createApiReference } from 'https://cdn.jsdelivr.net/npm/@scalar/api-reference/esm.js' createApiReference('#app', { ...config }) </script>createApiReference支持两种调用签名:只传配置,或传入挂载元素/选择器加配置(见 packages/types/src/api-reference/html-api.ts)。
经典 UMD 构建
以下三种情况会回退到经典 UMD 构建(通过<script src>加载,使用window.Scalar全局对象):
- 显式传入
cdnURL——例如用于固定某个特定版本的 UMD 构建; - 显式传入
bundle: false; - 设置了
nonce(严格 nonce 型 CSP,原因见下文 CSP 一节)。
UMD 默认 CDN 地址同样定义在源码中(见 src/html-rendering.ts):
export const DEFAULT_CDN = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference'生成的脚本形如:
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script> <script type="text/javascript"> Scalar.createApiReference('#app', { ...config }) </script>bundle 参数的优先级规则
bundle参数的完整语义(见 src/html-rendering.ts 与类型定义 packages/types/src/api-reference/html-rendering-configuration.ts):
bundle取值 | 效果 |
|---|---|
| 不设置(默认) | 加载 ESM 构建;若同时给了cdn或nonce,则回退 UMD |
true | 强制加载默认 ESM 构建(DEFAULT_ESM_CDN),优先级最高 |
'https://.../esm.js' | 加载指定的 ESM 构建 URL |
false | 强制回退到经典 UMD 构建 |
bundle的优先级高于cdn和nonce回退。测试 src/html-rendering.test.ts 验证了"同时传入 cdn 和 bundle: true 时,使用 ESM 而非 cdn 指定的 UMD"。
另外需要注意:由于各个框架集成会把未知的渲染选项展开合并进config对象,因此bundle也可能出现在config内部。renderApiReference会从config中读取它,同时确保它不会泄漏到序列化给客户端的配置里(见 src/html-rendering.ts),测试 src/html-rendering.test.ts 验证了这一行为。
Content Security Policy 与 nonce
对于安全要求较高的站点,nonce参数让 API Reference 可以在严格的script-src策略下运行(no unsafe-inline、no unsafe-eval)。
为什么设置 nonce 时默认回退 UMD
ESM 构建通过原生import加载懒加载 chunk,而import请求无法携带 nonce。因此,如果 CSP 策略是 strict-nonce 型(未开启'strict-dynamic'或未白名单 CDN 域名),ESM 的 chunk 请求会被 CSP 拦截。而单文件 UMD 构建只有一次<script>请求、没有后续请求,可以安全地打上 nonce,所以只要设置了nonce,默认就自动使用 UMD 构建。
如果你在 CSP 中启用了'strict-dynamic'(或显式白名单了 CDN 主机),可以传bundle: true强制切回 ESM 构建。
nonce 的完整行为
设置nonce后,生成 HTML 的行为包括(见 src/html-rendering.ts):
- 内联
<script>标签、CDN<script src>标签、<style>标签都会带上nonce="..."属性; - 额外输出
<meta property="csp-nonce" content="..." />,让独立 bundle 在运行时注入样式表时也能复用同一个 nonce(bundle 以useStrictCSP构建时会读取该 meta); - nonce 会经过属性转义(
"等),防止恶意 nonce 注入突破 HTML 属性边界——测试 src/html-rendering.test.ts 专门验证了这一点。
一个重要的限制
即使有了 nonce,style-src仍然需要'unsafe-inline'。原因是 API Reference 会在运行时渲染内联的style="..."属性,而 CSP nonce 只能授权<script>、<style>、<link>元素,无法授权内联属性。也就是说,nonce 带来的收益是完全严格的script-src,但样式策略仍需放宽。这一点在类型注释(packages/types/src/api-reference/html-rendering-configuration.ts)与 CHANGELOG(CHANGELOG.md)中均有明确说明。
使用示例:
const html = renderApiReference({ pageTitle: 'My API Reference', nonce: 'r4nd0m', // 需与你 script-src 指令中的 nonce-... 保持一致,且每次请求重新生成 config: { url: '/openapi.json' }, })测试 src/html-rendering.test.ts 验证了 nonce 场景下 UMD 回退、nonce 属性注入以及csp-noncemeta 的输出。
源码级原理:配置如何序列化进 HTML
renderApiReference生成的脚本中内嵌了序列化后的配置对象,这里有几个值得注意的实现细节。
serializeConfigToJs:函数也能跨边界存活
普通JSON.stringify会静默丢弃函数值,而 API Reference 的许多配置项本身就是函数(例如请求钩子onBeforeRequest、排序器tagsSorter/operationsSorter、事件回调onLoaded、onRequestSent等)。serializeConfigToJs(见 src/html-rendering.ts)解决了这个问题:
- 普通属性照常
JSON.stringify; - 函数属性通过
Function.prototype.toString()输出为字面量 JavaScript 源码,即"onBeforeRequest": ({ request }) => {...}; - 包含函数的数组(如
plugins)也会逐项以源码形式序列化(见 serializeArrayWithFunctions); - 输出的是一个合法的对象字面量,不会出现前导逗号等语法错误。
测试 src/html-rendering.test.ts 覆盖了tagsSorter、onBeforeRequest、redirect、generateModelSlug等十余种函数配置的保留,以及纯函数配置不产生非法前导逗号的场景(L377-L398)。
需要注意一个使用限制:函数必须是箭头函数或function表达式。对象方法简写(如onBeforeRequest(request) {})无法序列化为合法的独立表达式(见 src/html-rendering.ts 注释)。
getConfiguration:content 与 url 的取舍
如前文所述,getConfiguration会执行函数形式的content,并在同时给出url时移除content(见 src/html-rendering.ts),对应测试见 src/html-rendering.test.ts。
安全转义
renderApiReference对pageTitle和nonce做了双层防御:
escapeHtml转义&、<、>;escapeHtmlAttribute在 HTML 转义之外再编码双引号,防止属性逃逸(见 src/html-rendering.ts)。
测试 src/html-rendering.test.ts 验证了恶意标题<script>alert("xss")</script>会被安全转义。
在真实框架集成中的用法
renderApiReference是仓库内多个官方框架集成的公共底层。以 NestJS 集成为例,integrations/nestjs/src/nestJSApiReference.ts 的实现模式如下:
import { renderApiReference } from '@scalar/client-side-rendering' export function apiReference(givenConfiguration: NestJSReferenceConfiguration) { const configuration = { _integration: 'nestjs', ...givenConfiguration, } const content = () => { const { cdn, pageTitle, nonce, ...config } = configuration return renderApiReference({ config, pageTitle, cdn, nonce }, customThemeCSS) } // 支持 Express 与 Fastify 两种适配器,直接以 text/html 返回 if (givenConfiguration.withFastify) { return (_req: FastifyRequest, res: ServerResponse) => { res.writeHead(200, { 'Content-Type': 'text/html' }) res.write(content()) res.end() } } return (_req: Request, res: Response) => { res.send(content()) } }可以看到,renderApiReference的第二个参数被用于注入整套自定义主题 CSS(customThemeCSS,包含明暗双模式的颜色变量),且cdn、pageTitle、nonce等渲染选项从配置中剥离后单独传入,config则原样交给 API Reference。
仓库中同样直接依赖@scalar/client-side-rendering的集成还包括 Next.js(integrations/nextjs/src/ApiReference.ts)、Fastify(integrations/fastify/src/fastifyApiReference.ts)、Express(integrations/express/src/apiReference.ts)、Hono(integrations/hono/src/scalar.ts)、SvelteKit(integrations/sveltekit/src/scalar-api-reference.ts)、Astro 与 Docusaurus 等。无论你使用哪种服务端技术,都可以借鉴这一模式:剥离渲染选项、传入 config 与 customTheme,把返回的 HTML 字符串交给响应管道。
结合 Server-Side Rendering 包的取舍
仓库中还有一个配套的packages/server-side-rendering包,用于带水合(hydration)的服务端渲染场景。renderApiReference的源码注释对此做了明确区分(见 src/html-rendering.ts):
"For server-side rendering with hydration, use the server module instead."
选择建议:
- 纯 CDN 客户端渲染:页面内容由浏览器端 JS 渲染,服务端只返回静态 HTML 外壳。适合对首屏 SEO 要求不高、希望零服务端依赖快速接入的场景,也正是本文介绍的方式;
- 服务端渲染 + 水合:需要服务端完整渲染出 API 文档内容再交给浏览器接管,适合对首屏内容、SEO 与无 JS 环境有强要求的场景,此时应使用
server-side-rendering包。
小结
@scalar/client-side-rendering以极小的 API 面(一个renderApiReference函数)完成了"配置 → 完整 HTML 文档"的转换,其设计要点可以总结为:
- 零服务端依赖:只需要
@scalar/schemas、@scalar/types、@scalar/validation三个内部包,生成过程不触碰任何前端框架; - 默认 ESM、按需 UMD:默认加载 code-split 的 ESM 构建以优化首屏,
cdn/bundle: false/nonce三种情形自动回退单文件 UMD,bundle可显式强制选择; - 严格 CSP 友好:
nonce参数同时作用于内联脚本、CDN 脚本、样式标签与csp-noncemeta,代价仅是style-src仍需'unsafe-inline'; - 函数配置不丢失:
serializeConfigToJs把函数序列化为字面量 JavaScript,让onBeforeRequest、排序器、事件回调等跨过 HTML 边界存活; - 安全默认:标题、nonce 均做 HTML/属性转义,
url与content冲突时以url为准。
如果你想深入验证或二次开发,建议阅读 src/html-rendering.ts 与其配套测试 src/html-rendering.test.ts,测试覆盖了所有分支行为,是理解该包语义的最佳入口。
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考