在 React 项目中嵌入 Ruru:ruru-components 组件库使用指南与源码解析
【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal
导读
ruru-components 是 Graphile Crystal Monorepo 中Ruru(Grafast 风格 GraphiQL 发行版)背后的 React 组件库。当你想把一套开箱即用的 GraphQL IDE(含查询编辑、变量编辑、订阅支持、Explain 调试面板、文档浏览与历史记录)直接嵌进自己现有的 React 应用,而不是通过 ruru 独立服务器提供时,ruru-components 就是答案。本文将以 grafast/ruru-components/README.md 为主线,完整还原其最小接入流程与 Monaco workers 配置方案,并结合 源码 深入讲解 RuruProps 配置项、Explain 调试链路、事件流自动刷新与本地存储机制,帮助你真正"会用 + 懂原理"。
ruru-components 是什么
根据 grafast/ruru-components/package.json 的描述,ruru-components 是 "Grafast-flavoured GraphiQL distribution; the underlying React components"——即一个 Grafast 风味的 GraphiQL 发行版所依赖的底层 React 组件。
它的定位非常明确:ruru 本身是一个完整的 GraphQL IDE 应用(可独立运行/由服务器托管),而 ruru-components 把这些界面能力拆成可复用的 React 组件,供希望在既有 React 工程中嵌入 IDE 的开发者使用。README 原文如此概括:
The React components behind ruru, in case you want to embed Ruru into an existing React project.
从 src/index.tsx 可以看到,包的公开 API 极其精简,仅导出三样东西:
export type { Fetcher, RuruProps } from "./interfaces.ts"; export { Ruru } from "./ruru.tsx";也就是说,你真正需要关心的只有:一个组件<Ruru />和它的属性类型RuruProps(以及可选的Fetcher类型)。
快速开始:最小接入
安装与运行环境
ruru-components 发布在 npm 上(包名ruru-components)。根据其 package.json,运行环境需要满足:
- Node.js >= 22(
engines.node); - peerDependencies:
graphql ^16.9.0(需要自行安装); - React 19 运行时(
react/react-dom/react-compiler-runtime为其内部依赖)。
在已有 React 工程中,安装方式与普通依赖一致:
yarn add ruru-components graphql # 或 npm install ruru-components graphql最小使用示例
README 给出的核心用法如下(以下代码为原文档完整示例,可直接复制运行):
import "graphiql/style.css"; import "@graphiql/plugin-explorer/style.css"; import "ruru-components/ruru.css"; // Have Webpack include the Monaco workers import "graphiql/setup-workers/webpack"; // Or: import "graphiql/setup-workers/vite"; // Or: see "Monaco workers" below import { Ruru } from "ruru-components"; React.render(<Ruru endpoint="/graphql" />);这段代码蕴含了四个关键动作,下面逐一拆解:
- 导入三份样式表:GraphiQL 基础样式、Explorer 插件样式、ruru-components 自身的定制样式(
ruru.css)。包在 exports 字段 中专门导出了./ruru.css,说明样式是组件的一等公民,缺少它会直接导致布局错乱。 - 设置 Monaco workers:GraphiQL 的代码编辑器基于 Monaco(VS Code 的编辑器内核),其语言服务(JSON、GraphQL)运行在 Web Worker 中,必须显式装配。README 给出了 Webpack 与 Vite 两种自动方案,详见下文"Monaco workers 配置"。
- 导入
Ruru组件:这是唯一的顶层组件入口。 - 渲染组件:
<Ruru endpoint="/graphql" />声明式地指定 GraphQL 端点。注意endpoint可以传相对路径(如/graphql),组件内部会自动拼接为完整地址。
只传入一个 endpoint 就够了?
从 ruru-types/src/index.ts 的RuruProps定义看,endpoint是唯一"必需"的语义化属性,其余均为可选。组件内部(ruru.tsx)会为未显式传入的大量配置提供合理默认值,例如:
inputValueDeprecation默认true(提示已弃用的输入值);schemaDescription默认true;defaultQuery默认使用组件内置的 defaultQuery.ts;showPersistHeadersSettings默认true(提供"持久化 Headers"开关);- 同时会忽略
query、variables这两个已废弃的旧属性,并在RuruInner中预留了onEditQuery、responseTooltip、forcedTheme等 GraphiQL 扩展点(当前以注释形式标注,说明这些透传能力正在演进中)。
Monaco workers 配置
Monaco 编辑器会把 JSON 与 GraphQL 的语言解析放到 Worker 线程中执行。如果直接用<Ruru />而不同时装配 workers,编辑器往往表现为无语法高亮、无智能提示或直接报错。README 提供了两条路线:
路线一:交给打包器自动处理(推荐)
在 Webpack 工程中,只需一行副作用导入:
import "graphiql/setup-workers/webpack";在 Vite 工程中,则替换为:
import "graphiql/setup-workers/vite";这两种方式会分别利用 Webpack 的?worker模块规则与 Vite 的 worker 打包能力,把 Monaco 的editor.worker、json.worker、graphql.worker自动打包并注册到globalThis.MonacoEnvironment。
路线二:手动注入 MonacoEnvironment
如果打包器方案在你的工程中不可用(例如自定义构建链、CDN 分发、或 worker 策略受限),README 给出了手动方案——在引入 Ruru 之前,于 HTML 中加入如下<script type="module">块(完整代码,可原样复制):
<script type="module"> /* Set up monaco workers */ import createJSONWorker from "https://esm.sh/monaco-editor/esm/vs/language/json/json.worker.js?worker"; import createGraphQLWorker from "https://esm.sh/monaco-graphql/esm/graphql.worker.js?worker"; import createEditorWorker from "https://esm.sh/monaco-editor/esm/vs/editor/editor.worker.js?worker"; globalThis.MonacoEnvironment = { getWorker(_workerId, label) { switch (label) { case "json": return createJSONWorker(); case "graphql": return createGraphQLWorker(); default: return createEditorWorker(); } }, }; </script>其原理是:Monaco 在创建编辑器时会调用MonacoEnvironment.getWorker(workerId, label),我们根据label(语言标识)返回对应的 Worker 构造器——json返回 JSON 语言 worker,graphql返回 Monaco GraphQL 的 worker,其余语言一律回退到通用编辑器 worker。这段脚本必须先于Ruru 应用代码执行,否则 Monaco 找不到 worker 工厂。
RuruProps 完整配置项
ruru-components 的全部属性定义集中在 ruru-types/src/index.ts,ruru-components 只是从该 workspace 包原样再导出(见 src/interfaces.ts)。核心配置如下:
| 属性 | 类型 | 说明 |
|---|---|---|
endpoint | string | GraphQL HTTP 端点(http://或https://,也支持/graphql这类相对路径,默认/graphql) |
subscriptionEndpoint | string | GraphQL 订阅端点(ws://或wss://)。不传时,若提供了endpoint会自动推导出 ws 地址 |
fetcher | Fetcher | 可选覆盖默认 fetcher(来自@graphiql/toolkit),用于自定义请求/认证逻辑 |
debugTools | Array<"explain" \| "plan"> | 开放给用户的调试工具列表:explain(输出执行的 SQL)、plan(输出执行的计划) |
eventSourceInit | RuruEventSourceInit | 透传给new EventSource(url, eventSourceInit)的初始化参数。规范只定义withCredentials,但实现可扩展,例如reconnectInterval: 1000、maxReconnectAttempts: 3 |
editorTheme/defaultTheme | string | 编辑器主题与默认主题 |
maxHistoryLength | number | 历史记录最大条数 |
initialQuery/initialVariables/initialHeaders | string | 初始查询、变量、Headers |
defaultQuery/defaultHeaders | string | 默认查询与默认 Headers |
onEditQuery/onEditVariables/onEditHeaders | 函数 | 编辑回调 |
responseTooltip、defaultEditorToolsVisibility、isHeadersEditorEnabled、forcedTheme、confirmCloseTab、className | 透传 | 其余 GraphiQL 界面属性,原样转发给底层GraphiQLInterface |
endpoint 与订阅端点的自动推导
在 useFetcher.ts 中可以看到端点的处理逻辑:
- 若
endpoint以/开头,自动补全为window.location.origin + endpoint; - 订阅地址通过
makeWsUrl推导:相对路径补全为ws://或wss://(视当前页面协议而定),http(s)://前缀则直接替换为ws(s)://,其他形式原样使用; - 仅在显式传入
subscriptionEndpoint时才覆盖推导结果。
自定义 fetcher 与调试请求头
若未传fetcher,组件会用createGraphiQLFetcher创建默认 fetcher。当 Explain 功能开启时(详见下文),fetcher 会自动携带两个调试请求头:
headers["X-PostGraphile-Explain"] = "on"; headers["X-GraphQL-Explain"] = "plan,sql";这组请求头同时作为 WebSocket 连接的wsConnectionParams,保证订阅场景下 Explain 能力同样可用。
源码级架构:Ruru 组件内部长什么样
<Ruru />并非一个黑盒,其内部结构(src/ruru.tsx)清晰地分为三层:
<GraphiQLProvider> ← 全局状态(fetcher、插件、默认查询) <ExplainContext.Provider> ← Ruru 自定义:Explain 开关、结果与面板状态 <HistoryStore> ← 历史记录存储 <DocExplorerStore> ← 文档浏览存储 <RuruInner> ← GraphiQLInterface + 工具栏 + 页脚 + 错误弹窗内置插件集
Ruru 预置了 5 个 GraphiQL 插件:
| 插件 | 来源 | 用途 |
|---|---|---|
DOC_EXPLORER_PLUGIN | @graphiql/plugin-doc-explorer | 文档浏览器 |
DOWNLOAD_PLUGIN | 本地 plugins/download.tsx | 下载查询结果/请求 |
HISTORY_PLUGIN | @graphiql/plugin-history | 历史记录 |
explorerPlugin | @graphiql/plugin-explorer | 可视化 Schema 浏览器(关闭了归属水印showAttribution: false) |
EXPLAIN_PLUGIN | 本地 plugins/explain.tsx | Ruru 核心卖点:Explain 面板 |
工具栏与 Options 菜单
RuruInner在 GraphiQL 工具栏中注册了三个快捷键按钮:
- Prettify Query(Shift-Ctrl-P):由 usePrettify.tsx 实现——优先动态加载
prettier/standalone及其 estree/babel/graphql 插件,用prettier.formatWithCursor分别以graphql(查询)和jsonc(变量/Headers)解析器格式化三个编辑器,并保留光标位置;若 Prettier 在 2 秒内加载失败则回退到 GraphiQL 内置的 prettify 动作; - Merge Query(Shift-Ctrl-M):合并查询片段;
- Copy query(Shift-Ctrl-C):复制当前查询。
工具栏右侧的Options下拉菜单由本地状态驱动,包含五项开关(详见下一节)。
错误弹窗
当 EventSource 连接异常等运行时错误发生时,ErrorPopup.tsx 会以浮层形式展示错误信息(源码注释也坦承该组件"需要更完善的设计与无障碍支持",属于仍在打磨的部分)。
Options 菜单与本地存储机制
Options 菜单中的每个开关都与localStorage双向绑定,存储实现位于 useStorage.ts。所有键均以Ruru:为前缀(除graphiql:explorerIsOpen复用了 GraphiQL 既有键):
| 菜单项 | 存储键 | 值 | 效果 |
|---|---|---|---|
| Explain (if supported) | Ruru:explain | "true"/"" | 开启后向 fetcher 注入调试请求头 |
| Verbose | Ruru:verbose | "true"/"" | 是否在响应中保留extensions.explain(不开启则隐藏) |
| Condensed | Ruru:condensed | "true"/"" | 为界面追加condensedclass,压缩垂直空间 |
| onError: PROPAGATE | Ruru:onError | "PROPAGATE" | 传统 GraphQL 错误处理(错误正常传播) |
| onError: NULL | Ruru:onError | "NULL" | 客户端负责错误处理(对应 Grafast 的 null 语义) |
| onError: HALT | Ruru:onError | "HALT" | 遇到首个错误立即停止执行 |
其中onError是 Grafast 执行语义在界面层的暴露:wrappedFetcher会把存储中的onError值作为附加参数合并进每个 fetcher 请求(见 useFetcher.ts 中{ ...params, onError }),从而让用户在不改服务端代码的情况下切换 Grafast 的错误处理策略。
RuruStorage接口还提供了toggle(key)便捷方法,内部实现针对condensed做了特判(默认为开)。所有set操作都会递增一个 revision state 触发重渲染,保证 UI 与存储即时同步。
Explain:Ruru 的调试核心
Explain 是 Ruru 区别于普通 GraphiQL 发行版的关键能力,也是 ruru-components 中最值得深入的部分。
启用条件
在 useFetcher.ts 中,Explain 的启用需要同时满足:
const explain = options.explain && (!props.debugTools || props.debugTools.includes("explain"));即:用户在 Options 菜单中打开了 Explain 开关且(未限制debugTools或debugTools中包含了"explain")。若你希望完全屏蔽调试能力,可显式传debugTools={[]}。
数据流
- 开启后,fetcher 携带
X-PostGraphile-Explain: on与X-GraphQL-Explain: plan,sql请求头; - 服务端(PostGraphile / Grafast)在响应
extensions.explain中返回结构化的解释结果,形如:
interface ExplainResults { operations: Array< | { type: "sql"; query: string; explain?: string } // SQL 操作 | { type: "plan"; plan: GrafastPlanJSON } // 计划操作 >; }wrappedFetcher在每次非 introspection 响应后:若extensions.explain格式合法,则延迟 100ms 将其写入 state 并渲染到 Explain 面板;同时若未开启 Verbose,会通过Object.defineProperty把explain属性改为不可枚举(hideProperty),做到"从结果中隐藏、但面板可见";- 若响应携带的是旧版 PostGraphile v4 的顶层
explain数组,则兼容转换为type: "sql"的 Legacy explain 条目; - introspection 查询会被短路直接返回,避免干扰用户视图。
Explain 面板
plugins/explain.tsx 把 Explain 注册为带放大镜图标的 GraphiQL 插件,其内容由 components/Explain.tsx 渲染。面板的尺寸与位置偏好通过 useExplain.ts 持久化到Ruru:explainIsOpen、Ruru:explainSize(默认 300px)、Ruru:explainAtBottom(默认在底部)等键中。
事件流:Schema 变更自动刷新
ruru-components 还内置了对 GraphQL Live/Schema 变更事件流的支持。机制如下(useGraphQLChangeStream.ts + useFetcher.ts):
- 默认 fetcher 包装了
window.fetch,每次响应都会检查X-GraphQL-Event-Stream响应头; - 若存在,则以该响应头值为地址创建
EventSource(支持通过eventSourceInit传入withCredentials、reconnectInterval等扩展选项); - EventSource 收到
change事件时,自动触发一次 introspection(refetch),使编辑器内的 Schema 文档与智能提示实时跟随服务端变更; - 连接异常(如服务端重启导致 WebSocket 意外终止)时,会以友好错误信息提示用户,而不是输出原始的
{"isTrusted": true}噪音。
这套设计让 Ruru 在开发态(schema 热更新)与生产态(实时 schema 演进)下都能保持文档与提示的最新状态。
其他使用模式与进阶参考
README 明确指出:其他使用模式请参考主包 ruru。ruru 提供了独立运行、CLI 与服务器托管等更完整的形态,相关文档见 grafast/ruru/README.md(其服务端集成、CLI 用法与 HTML 分发方案见 grafast/ruru/src/server.ts、grafast/ruru/src/cli.ts)。
如果你的诉求是"最快的现成 IDE",优先使用 ruru 独立包;如果你的诉求是"把 IDE 深度嵌进自己的 React 应用、并自定义 fetcher 与调试工具",ruru-components 就是那个正确的切入点。二者共享同一套 RuruProps 语义,迁移成本很低。
结语
ruru-components 以极小的公开 API(Ruru+RuruProps)封装了一个功能完整的 Grafast 风格 GraphQL IDE:开箱即用的五个插件、Monaco workers 的三种装配方案、endpoint驱动的自动 fetcher、Explain 调试链路,以及基于localStorage的完整偏好持久化。通过本文对照 README 与 src/ruru.tsx、src/hooks、ruru-types/src/index.ts 等源码阅读,你既可以按最小示例快速接入,也能深入理解其请求头、错误语义与事件流机制,进而在自己的产品中定制出符合需求的 GraphQL 工作台。
【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考