Scalar Client-Side Rendering 实战:用 CDN 零依赖渲染 API Reference 静态 HTML
2026/9/15 14:45:27 网站建设 项目流程

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是函数,会先执行它,把返回值作为实际内容;
  • 若同时提供了contenturlurl优先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全局对象):

  1. 显式传入cdnURL——例如用于固定某个特定版本的 UMD 构建
  2. 显式传入bundle: false
  3. 设置了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 构建;若同时给了cdnnonce,则回退 UMD
true强制加载默认 ESM 构建(DEFAULT_ESM_CDN),优先级最高
'https://.../esm.js'加载指定的 ESM 构建 URL
false强制回退到经典 UMD 构建

bundle的优先级高于cdnnonce回退。测试 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-inlineno 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):

  1. 内联<script>标签、CDN<script src>标签、<style>标签都会带上nonce="..."属性;
  2. 额外输出<meta property="csp-nonce" content="..." />,让独立 bundle 在运行时注入样式表时也能复用同一个 nonce(bundle 以useStrictCSP构建时会读取该 meta);
  3. nonce 会经过属性转义(&quot;等),防止恶意 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、事件回调onLoadedonRequestSent等)。serializeConfigToJs(见 src/html-rendering.ts)解决了这个问题:

  • 普通属性照常JSON.stringify
  • 函数属性通过Function.prototype.toString()输出为字面量 JavaScript 源码,即"onBeforeRequest": ({ request }) => {...}
  • 包含函数的数组(如plugins)也会逐项以源码形式序列化(见 serializeArrayWithFunctions);
  • 输出的是一个合法的对象字面量,不会出现前导逗号等语法错误。

测试 src/html-rendering.test.ts 覆盖了tagsSorteronBeforeRequestredirectgenerateModelSlug等十余种函数配置的保留,以及纯函数配置不产生非法前导逗号的场景(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。

安全转义

renderApiReferencepageTitlenonce做了双层防御:

  • 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,包含明暗双模式的颜色变量),且cdnpageTitlenonce等渲染选项从配置中剥离后单独传入,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 文档"的转换,其设计要点可以总结为:

  1. 零服务端依赖:只需要@scalar/schemas@scalar/types@scalar/validation三个内部包,生成过程不触碰任何前端框架;
  2. 默认 ESM、按需 UMD:默认加载 code-split 的 ESM 构建以优化首屏,cdn/bundle: false/nonce三种情形自动回退单文件 UMD,bundle可显式强制选择;
  3. 严格 CSP 友好nonce参数同时作用于内联脚本、CDN 脚本、样式标签与csp-noncemeta,代价仅是style-src仍需'unsafe-inline'
  4. 函数配置不丢失serializeConfigToJs把函数序列化为字面量 JavaScript,让onBeforeRequest、排序器、事件回调等跨过 HTML 边界存活;
  5. 安全默认:标题、nonce 均做 HTML/属性转义,urlcontent冲突时以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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询