如何注册文件预览器:DSH-better-sidebar的FileViewer API完整参考(匹配算法/取数策略/真实案例)
【免费下载链接】DSH-better-sidebar开放的侧边栏底座,支持三方拓展注册新侧边栏页面。内置文件渲染编辑/终端/侧边对话/Git/子代理页面 | Open sidebar foundation, supports third-party extensions to register new sidebar pages. Built-in file rendering/editing, terminal, side chat, Git, and sub-agent pages.项目地址: https://gitcode.com/gh_mirrors/ds/DSH-better-sidebar
DSH-better-sidebar 是一个开放侧边栏底座,内置文件编辑/预览、终端、Git、子代理与侧边对话页面,并开放ctx.betterSidebar服务给第三方插件。本文聚焦其中最实用的FileViewer API(registerFileViewer):如何用不到 20 行代码让你的插件在侧边栏中预览.csv、.parquet等任意文件类型,并彻底讲清匹配算法(matchFileViewer)、5 种取数策略(fetchStrategy)与仓库内的真实案例。
📖 完整接入文档见 外部插件接入指南,内置预览器参考实现在 src/client/builtins/viewers.tsx。
一、FileViewer 能做什么:内置预览器全是这么注册的
在 DSH-better-sidebar 里,侧边栏能预览什么文件,完全由"注册表"决定。打开一个文件时,宿主按你注册的扩展名/内容嗅探规则找到对应的预览器组件,再按你声明的取数策略把文件内容喂给它。
关键在于:内置的 6 个预览器(image / pdf / markdown / html / code / binary-download)自己也是通过同一套 FileViewer API 注册的("吃自己的狗粮")。这意味着:
- 你的插件预览器和内置功能完全对等:同样出现在设置页「侧边卡片」中,可被用户独立启停;
- 想覆盖内置行为?注册同扩展名 + 更高
priority即可,无需改宿主代码。
二、三步注册一个文件预览器(最小骨架)
注册只需三步:声明依赖 → 包在ctx.effect里 → 调用registerFileViewer。
// 你的插件 client 入口(完整示例见指南 §3 / §13) export const inject = ['betterSidebar'] // 保证 better-sidebar 先就绪 export function apply(ctx: Context): void { ctx.effect(() => ctx.betterSidebar.registerFileViewer({ id: 'my-plugin:csv', // 建议带包前缀,避免 id 冲突 exts: ['csv'], // 小写、不带点的扩展名 fetchStrategy: 'custom', // 取数策略,见下节 load: async (path, scope) => parseCsv(await fetchText(scope, path)), component: ({ customData, path }) => <CsvGrid data={customData} />, }) ) }⚠️两个必踩的坑:
- 注册必须包在
ctx.effect(...)里。注册函数返回的 disposer 会在插件卸载/HMR 时自动执行;不包 effect 的话,再次激活会直接抛"already registered"; - 不要 value-import
dsh-better-sidebar。跨插件交互只走ctx.betterSidebar方法调用(构建纯度门会拦截),类型用import type {}导入即可。
三、FileViewerDescriptor 完整字段速查
| 字段 | 作用 | 缺省行为 |
|---|---|---|
id | 唯一标识,也是设置页开关的 key | 必填,重复注册会抛错 |
exts | 认领的扩展名数组(小写无点);[]= catch-all 兜底 | 必填 |
priority | 优先级,高者优先裁决 | 0 |
fetchStrategy | 字节获取策略(5 选 1,见下节) | 必填 |
detect | 内容嗅探:(path, head) => boolean,按文件头字节认领,可忽略扩展名 | 无 |
load | fetchStrategy: 'custom'时的加载函数,支持AbortSignal中止 | 无 |
title/icon | 设置清单展示名与图标(支持 i18n 函数形式) | 回退到id |
settings | 声明式设置行(开关/数值/下拉/自定义面板),自动渲染到设置页 | 无 |
component | 渲染函数,接收FileViewerProps | 必填 |
组件收到的FileViewerProps里,你真正关心的是path、scope(会话标识,调 API 必带)、以及按策略填充的content/mediaUrl/customData三选一。
四、5 种 fetchStrategy 取数策略:字节从哪来
这是新手最常问的问题——"我的组件怎么拿到文件内容?"答案就在策略选择里:
| 策略 | 字节来源 | 传给组件的字段 | 适用场景 |
|---|---|---|---|
none | 不需要字节 | 无 | 纯 UI 自渲染 |
fsRead | 宿主/sidebar/api的fs.read | content、truncated | 文本类:CSV / JSON / XML |
mediaUrl | /sidebar/file媒体路由 URL | mediaUrl | 图片 / PDF(viewer 自己 fetch) |
custom | 你写的load()函数 | customData | 远程拉取、需二次解析的格式 |
binary-download | 不预览,直接显示下载按钮 | 无 | 无客户端渲染器的二进制 |
选型建议:
- 能当文本读的格式(CSV、JSON、日志)→
fsRead,宿主帮你读,零网络代码; - 媒体文件(图、PDF)→
mediaUrl,拿到一个同源 URL 直接塞给<img>/ PDF 组件; - 需要解析成结构(Parquet、YAML、自定义协议)或数据不在本地→
custom+load(),插件内用fetch('/sidebar/api/fs.read')拉字节即可(响应体是{ value: ... }包装); - 真·二进制(exe、压缩包)→
binary-download,给用户一个体面的下载按钮而不是乱码。
五、匹配算法 matchFileViewer 深度解析:谁抢到了这个文件?
当你打开一个文件时,宿主用matchFileViewer(path, head?)决定交给谁渲染。源码在 src/client/service.ts,核心规则一句话:按 priority 降序单趟遍历,每个描述符在自己的回合里先试detect、再试exts,先命中者赢。
拆开看:
- 单趟 + 优先级降序(稳定排序,同优先级按注册先后)。高
priority的描述符先获得裁决权——它的detect或exts任一命中即赢,低优先级的规则没机会参与; - detect 优先于 exts:若文件头字节(
head)可用且你声明了detect,先跑嗅探;嗅探失败时,exts: []的纯嗅探型本轮直接放弃(防止"magic number 预览器吞掉所有文件"); - catch-all 语义:
exts: []且无detect是"盲兜底",命中任何路径——内置的code预览器正是用它以-100的最低优先级接住所有漏网文件; - head 字节从哪来:第一次匹配(纯扩展名)没有 head。若文件读出来是二进制,宿主
fs.read响应会带head字段(base64 前 4KB),编辑器拿它对detect型预览器重匹配一次。所以嗅探型预览器的实际触发场景是"扩展名落空 / 二进制文件"; - 全部落空返回
undefined,编辑器显示下载按钮;在设置页被用户禁用的预览器整轮跳过。
内置 6 个预览器的优先级全景
| 预览器 | priority | exts | 策略 | 备注 |
|---|---|---|---|---|
image | 0 | png/jpg/gif/webp/svg/… | mediaUrl | 常见图片格式 |
pdf | 0 | mediaUrl | 浏览器原生查看器内嵌 | |
markdown | 0 | md/markdown | fsRead | 含 Mermaid、内嵌 HTML 消毒、浮动目录 |
html | 0 | html/htm | fsRead | 沙箱 iframe 预览 |
binary-download | -50 | doc/xls/ppt +NUL 嗅探 | binary-download | detect检查 head 含 NUL 字节 |
code | -100 | []catch-all | fsRead | CodeMirror 文本编辑兜底 |
💡覆盖套路:想自定义
.svg渲染,注册exts: ['svg'], priority: 10即可压过内置image;想按 magic bytes 认领无扩展名的 Parquet,注册exts: [], priority: 100, detect: (p, head) => head[0] === 0x50 …('PAR1' 魔数)。
六、真实案例:仓库里的预览器是怎么长的
案例 1:Office 三件套预览(.docx / .xlsx / .pptx)——官方不再内置,而是拆成推荐插件,通过设置页「添加插件」→ 文件预览弹窗安装,插件以相同 id 走同一套 API 注册。目录数据源在 src/client/plugins-viewers.ts:加一条数据即"上架",tests/plugin-list.spec.ts 守护。
案例 2:内置binary-download——src/client/binary-download.tsx 配detect: (_path, head) => head.includes(0):文件头含 NUL 即判定二进制,在 head 重匹配阶段抢在code兜底之前认领,给用户一个下载按钮而非乱码页。
案例 3:三方插件生态——首批通过ctx.betterSidebar接入的三方插件(dsh-sentinel 监控台、dsh-sidebar-qa 划选追问)都以"可选软依赖"方式注册,better-sidebar 未安装时注册静默跳过,宿主零侵入。
七、实战提示与避坑清单
| 陷阱 | 说明 |
|---|---|
注册没包ctx.effect | HMR / 插件禁用后注册残留,再次激活抛"already registered" |
| id 与内置冲突 | 6 个内置 viewer id 不可重复注册;务必用my-plugin:xxx前缀 |
| catch-all 慎用 | exts: []会接走所有未被更高优先级认领的文件;只想补几个扩展名就用exts精确列举 |
| 文本文件别指望 detect | 嗅探只在二进制 head 重匹配时触发;文本格式直接用exts或custom策略 |
| 别 import 内部模块 | src/client/api.ts是内部封装,外部插件按指南 §6 的 fetch 模式自己请求/sidebar/api/* |
配套阅读:
- 匹配算法与生命周期测试:tests/service.spec.ts / tests/builtins.spec.ts(内置清单断言 7 tab + 6 viewer)
- 声明式设置页(你的 viewer 自动出现在这里):src/client/SideCardSection.tsx
- 完整最小示例(Database tab + CSV viewer):指南 §13
【免费下载链接】DSH-better-sidebar开放的侧边栏底座,支持三方拓展注册新侧边栏页面。内置文件渲染编辑/终端/侧边对话/Git/子代理页面 | Open sidebar foundation, supports third-party extensions to register new sidebar pages. Built-in file rendering/editing, terminal, side chat, Git, and sub-agent pages.项目地址: https://gitcode.com/gh_mirrors/ds/DSH-better-sidebar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考