使用场景
JsonViewer 组件可用于 JSON 数据的展示与编辑。Semi 重点参考了 VS Code的 text-buffer 数据结构设计思路,复用了部分 utils与数据类型定义(Token解析,语言服务等),结合我们的功能/样式定制需求,实现了 JsonViewer 组件, 视觉上会与 Semi Design Next 体系内的其他组件更协调,对于特定数据类型的定制化渲染定制会更方便。
相比于直接使用 MonacoEditor,Semi JsonViewer 在工程化构建上做了额外处理,使用更为简单,无需关注 Webpack插件、worker loader等复杂的配置。
同时由于我们仅关注 Json 数据格式,更轻量化,在开箱即用的同时,拥有更小的体积(📦 -96%) ,更极致的加载速度(🚀 -53.5%) ,更少的内存占用(⬇️ 71.6%)。
对于五百万行及以下的数据,均可以做到1s内完成数据加载与解析。详细的对比数据可查阅 Performance 章节
- 如果你仅需要对 Json 做预览/编辑,无需对更复杂的其他编程语言作修改,我们建议你选用 JsonViewer
- 如果你还需要处理其他格式的数据/代码文件,完整的代码编辑器能力(语法高亮、代码补全、错误提示、复杂编辑等)是刚需,构建产物体积不是关注重点,我们建议你选用 Monaco Editor
代码演示
如何引入
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
</script>
基本用法
JsonViewer 的基本用法。传入 height 和 width 设置组件的高度和宽度,通过 value 传入 Json 字符串作为初始值。编辑内容变化时触发 onChange,参数为编辑器当前文本。value 可以 bind:value,变为与编辑器当前内容不同的值时才会重建编辑器,回写同一内容不会丢失光标;不绑定时编辑器保留用户的输入。若需要获取组件的值,可以通过 bind:this(类型 JsonViewerHandle)调用 getValue(),具体可参考 Methods。
代码
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={100} value={data} width={700} />
</div>设置行高
配置 options 的 lineHeight 参数,设置固定行高(单位:px, 默认 20)。
代码
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
const wrapperStyle = 'margin-bottom: 12px; overflow: hidden';
</script>
<div>
<div style={wrapperStyle}>
<JsonViewer height={100} options={{ lineHeight: 20 }} value={data} width={700} />
</div>
<div style={wrapperStyle}>
<JsonViewer height={120} options={{ lineHeight: 24 }} value={data} width={700} />
</div>
<div style={wrapperStyle}>
<JsonViewer height={120} options={{ lineHeight: 26 }} value={data} width={700} />
</div>
</div>自动换行
配置 options 的 autoWrap 参数,设置为 true 时,组件会根据内容长度自动换行。
代码
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0",
"description": "Semi Design is a design system that defines a set of mid_back design and front_end basic components."
}`;
const options = { autoWrap: true };
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={120} {options} value={data} width={700} />
</div>格式化配置
配置 options 的 formatOptions 参数,设置组件的格式化配置。
- tabSize: number,设置缩进大小为4,表示每级缩进 4 个空格
- insertSpaces: boolean,true 表示使用空格进行缩进,false 表示使用制表符(Tab)
- eol: string,设置换行符,可以是\n,\r\n,
代码
<script lang="ts">
import { Button, JsonViewer, type JsonViewerHandle } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
const options = { formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' } };
let jsonviewer = $state<JsonViewerHandle>();
</script>
<div>
<Button onclick={() => jsonviewer?.format()}>格式化</Button>
<div style="margin-bottom: 16px; margin-top: 16px">
<JsonViewer bind:this={jsonviewer} height={100} {options} value={data} width={700} />
</div>
</div>自定义渲染规则
通过配置 options.customRenderRule 参数,你可以自定义 JSON 内容的渲染方式(注意:仅在只读模式下生效)。
行为说明
customRenderRule在计算path时更精确:同一条键值对的 key token 与 value token 会拥有相同的path(即都对应到该属性所在的路径,例如root.<key>)。因此,像
path === 'root.<key>'这类仅依赖path的规则,可能同时命中 key 和 value,导致与后续 value 匹配规则产生“覆盖/优先级”差异。若你希望只匹配 key 或只匹配 value,请使用函数匹配的第三个参数
tokenType:const targetPath = 'root.<key>'; // 仅匹配 key match: (_value, path, tokenType) => tokenType === 'key' && path === targetPath; // 仅匹配 value match: (value, path, tokenType) => tokenType === 'value' && path === targetPath;
customRenderRule 是一个规则数组,规则只保存数据:match 为匹配条件,可选的 name 用于在 customRender snippet 里区分规则。命中 token 的内容由 {#snippet customRender({ item, index, content })} 绘制,item 是命中的规则,index 是它在 customRenderRule 中的序号。
match: 匹配条件,可以是以下三种类型之一:- 字符串:精确匹配
- 正则表达式:按正则匹配
- 函数:自定义匹配逻辑,函数签名为
(value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => booleanvalue: 待匹配的值(JSON 键或值)。当match为函数时,value会尽量传入解析后的原始类型(number / boolean / null / string),因此可以使用===进行严格匹配;当match为字符串或正则时,仍基于文本内容匹配(字符串类型会去除两侧引号)path: 当前匹配到的路径,格式为root.key1.key2.key3[0].key4tokenType: 当前 token 的类型,'key'表示 JSON 键名,'value'表示 JSON 值。可用于区分同名键和值的匹配
name: 可选,规则名称,在customRendersnippet 中用item.name区分规则customRendersnippet:参数为{ item, index, content }content: 匹配到的内容。如果是字符串类型的值,将包含双引号(如"name","Semi")- 未提供 snippet 或 snippet 什么都不画时不做自定义渲染,保留 token 原文
代码
<script lang="ts">
import { Image, JsonViewer, Popover, Rating, Tag, type JsonViewerOptions } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "2.7.4",
"rating": 5,
"tags": ["design", "react", "ui"],
"image": "/assets/root-web-sites/abstract.jpg"
}`;
// A rule holds data only; the customRender snippet draws the token and tells the rules apart by name
const options: JsonViewerOptions = {
formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' },
customRenderRule: [
{ name: 'popover', match: 'Semi' },
{ name: 'rating', match: (value) => value == 5 },
{
name: 'tag',
match: (_value, path) => path === 'root.tags[0]' || path === 'root.tags[1]' || path === 'root.tags[2]',
},
{ name: 'image', match: /^http/ },
],
readOnly: true,
autoWrap: true,
};
// content is the raw string token with its quotes; without them it is a valid URL
const unquote = (content: string) => content.replace(/^"|"$/g, '');
</script>
{#snippet imagePreview(src: string)}
<Image height={100} {src} width={100} />
{/snippet}
<div>
<div style="margin-bottom: 16px; margin-top: 16px">
<JsonViewer height={200} {options} showSearch={false} value={data} width={600}>
{#snippet customRender({ item, content: text })}
{#if item.name === 'popover'}
<Popover showArrow trigger="hover">
{#snippet content()}我是用户自定义的渲染{/snippet}
<span>{text}</span>
</Popover>
{:else if item.name === 'rating'}
<Rating disabled size={10} value={Number(text)} />
{:else if item.name === 'tag'}
<Tag shape="circle" size="small">{text}</Tag>
{:else}
<Popover showArrow trigger="hover">
{#snippet content()}{@render imagePreview(unquote(text))}{/snippet}
<span>{text}</span>
</Popover>
{/if}
{/snippet}
</JsonViewer>
</div>
</div>自定义搜索按钮
通过 searchButton snippet,你可以自定义搜索按钮的渲染方式,实现固定位置、自定义样式等需求。提供 snippet 即整体替换默认按钮,defaultButton 是默认按钮的 snippet,用 {@render defaultButton()} 渲染;showSearch 为 false 时两者都不渲染。
代码
<script lang="ts">
import { IconSearch } from '@fibrae/semi-icons-svelte';
import { Button, JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={200} value={data} width={700}>
{#snippet searchButton({ defaultButton, showSearchBar, onToggleSearchBar })}
<div style="position: absolute; top: 10px; right: 10px; z-index: 10">
{#if !showSearchBar}
<Button onclick={onToggleSearchBar}>
{#snippet icon()}
<IconSearch />
{/snippet}
搜索
</Button>
{:else}
{@render defaultButton()}
{/if}
</div>
{/snippet}
</JsonViewer>
</div>API 参考
JsonViewer
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| customRender | 替换被 options.customRenderRule 命中的 token 的内容,snippet 参数为命中的规则 item、规则序号 index 与 token 原文 content;snippet 什么都不画时保留原文。仅在只读模式下生效。 | TypeSnippet<[JsonViewerCustomRenderArgs]> | - |
| height | 高度,数字按 px 处理,字符串原样使用。 | number | string | 400 |
| limitSearchButtonBounds | 是否限制搜索按钮的拖动范围在组件容器内。 | boolean | false |
| onChange | 编辑内容变化时调用,参数为编辑器当前文本。 | Type(value: string) => void | - |
| options | 编辑器配置,字段见 JsonViewerOptions。ariaLabel、readOnly、autoWrap 的默认值按键合并,只传部分字段时其余仍取默认值。内容按深比较变化时重建编辑器;就地修改 formatOptions、customRenderRule 等嵌套对象不会被察觉,需替换为新对象。 | JsonViewerOptions | - |
| searchButton | 整体替换默认搜索按钮;defaultButton 是默认按钮的 snippet,用 {@render defaultButton()} 渲染,其余参数用于自建搜索界面。showSearch 为 false 时不渲染。 | TypeSnippet<[JsonViewerSearchButtonArgs]> | - |
| showSearch | 是否显示搜索按钮。 | boolean | true |
| value | 编辑器的文本,配合 bind:value 使用;变为与编辑器当前内容不同的值时重建编辑器,与当前内容相同的回写不会丢失光标;未传时按空文本创建。 | string | - |
| width | 宽度,数字按 px 处理,字符串原样使用。 | number | string | 400 |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| format | 按 formatOptions 格式化当前内容。 | Type() => void |
| getSearchResults | 获取当前搜索结果。 | Type() => ReturnType<JsonViewerFoundation['getSearchResults']> |
| getStyle | 返回当前的 width 与 height。 | Type() => { width: string | number; height: string | number; } |
| getValue | 获取编辑器当前文本。 | Type() => string |
| nextSearch | 跳转到下一个搜索结果,可选步长。 | Type(step?: number) => void |
| prevSearch | 跳转到上一个搜索结果,可选步长。 | Type(step?: number) => void |
| replace | 替换当前搜索匹配项;替换文本为空串时删除匹配,没有匹配时不做任何事。 | Type(replaceText: string) => void |
| replaceAll | 替换所有搜索匹配项;替换文本为空串时删除全部匹配。 | Type(replaceText: string) => void |
| search | 搜索文本,可选参数控制大小写敏感、全词匹配和正则表达式。 | Type(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) => void |
class、style 与原生属性落在根元素上。编辑器自己管理根元素的内容,所以没有 children。根元素没有 role、而传了 aria-label 或 aria-labelledby 时,根元素加 role="group";显式传入的 role 优先。
JsonViewerOptions
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| lineHeight | 行高 | number | 20 |
| autoWrap | 是否自动换行 | boolean | true |
| readOnly | 是否只读 | boolean | false |
| customRenderRule | 自定义渲染规则 | CustomRenderRule[] | - |
| formatOptions | 格式化配置 | FormattingOptions | - |
| completionOptions | 自动补全配置 | CompletionOptions | - |
| ariaLabel | 内容区的可访问名称(只读时的滚动区域、可编辑时的文本框) | string | 语言包 JsonViewer.content |
CustomRenderRule
类型名为 JsonViewerCustomRenderRule,规则只保存数据,命中 token 的内容由 customRender snippet 绘制。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| match | 匹配规则 | string | RegExp | (value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => boolean | - |
| name | 规则名称,在 customRender snippet 中区分规则 | string | - |
FormattingOptions
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| tabSize | 缩进大小 | number | 4 |
| insertSpaces | 是否使用空格进行缩进 | boolean | true |
| eol | 换行符 | string | '\n' |
CompletionOptions
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| staticCompletions | 静态补全项。只在按文档结构已推断出候选项时追加在其后,推断不出任何候选项时不提供;与已有候选项同 label 的不重复添加。只读取每项的 label、insertText(缺省时用 label)、documentation | CompletionItem[] | - |
SearchControls
使用 searchButton snippet 时,参数(类型 JsonViewerSearchButtonArgs)除 defaultButton 外还包含以下属性:
| 属性 | 说明 | 类型 |
|---|---|---|
| showSearchBar | 当前是否显示搜索栏 | boolean |
| onToggleSearchBar | 切换搜索栏显示/隐藏 | () => void |
| onSearch | 执行搜索 | (text: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) => void |
| onPrevSearch | 跳转到上一个搜索结果 | () => void |
| onNextSearch | 跳转到下一个搜索结果 | () => void |
| onReplace | 替换当前搜索结果 | (text: string) => void |
| onReplaceAll | 替换所有搜索结果 | (text: string) => void |
Methods
通过 bind:this(类型 JsonViewerHandle)可以调用组件导出的方法,例如 jsonviewer?.format()。
| 名称 | 描述 |
|---|---|
| getValue() | 获取当前值 |
| format() | 格式化当前内容 |
| search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) | 搜索文本,可选参数控制大小写敏感、全词匹配和正则表达式 |
| getSearchResults() | 获取当前搜索结果 |
| prevSearch(step?: number) | 导航到上一个搜索结果,可选步长参数 |
| nextSearch(step?: number) | 导航到下一个搜索结果,可选步长参数 |
| replace(replaceText: string) | 替换当前搜索匹配项 |
| replaceAll(replaceText: string) | 替换所有搜索匹配项 |
| getStyle() | 获取当前的 width 与 height |
Performance
Bundle Size
| 组件 | 体积 | 体积(Gzip) |
|---|---|---|
| JsonViewer | 203.14kb | 51.23kb |
| MonacoEditor | 5102.0 KB | 1322.7 KB |
渲染不同量级数据耗时
注:
- 测试数据生成方式详情可查阅 url
- 当数据量级超出50w行时,ReactMonacoEditor 默认关闭高亮等行为,数据对比不遵循单一变量原则
| 组件 | 1k行 | 5k行 | 1w行 | 10w行 | 50w行 | 100w行 | 300w行 |
|---|---|---|---|---|---|---|---|
| JsonViewer | 30.42ms | 30.66ms | 36.87ms | 52.73ms | 111.02ms | 178.81ms | 506.25ms |
| ReactMonacoEditor | 72.01ms | 73.76ms | 76.64ms | 97.89ms | 133.31ms | 202.79ms | 495.53ms |
| 性能提升 | 57.70% | 58.41% | 51.87% | 46.11% | - | - | - |