Plus 组件

Json编辑器

用于展示和编辑 JSON 数据

使用场景

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') => boolean
      • value: 待匹配的值(JSON 键或值)。当 match 为函数时,value 会尽量传入解析后的原始类型(number / boolean / null / string),因此可以使用 === 进行严格匹配;当 match 为字符串或正则时,仍基于文本内容匹配(字符串类型会去除两侧引号)
      • path: 当前匹配到的路径,格式为 root.key1.key2.key3[0].key4
      • tokenType: 当前 token 的类型,'key' 表示 JSON 键名,'value' 表示 JSON 值。可用于区分同名键和值的匹配
  • name: 可选,规则名称,在 customRender snippet 中用 item.name 区分规则
  • customRender snippet:参数为 { 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 | string400
limitSearchButtonBounds是否限制搜索按钮的拖动范围在组件容器内。booleanfalse
onChange编辑内容变化时调用,参数为编辑器当前文本。Type(value: string) => void-
options编辑器配置,字段见 JsonViewerOptions。ariaLabel、readOnly、autoWrap 的默认值按键合并,只传部分字段时其余仍取默认值。内容按深比较变化时重建编辑器;就地修改 formatOptions、customRenderRule 等嵌套对象不会被察觉,需替换为新对象。JsonViewerOptions-
searchButton整体替换默认搜索按钮;defaultButton 是默认按钮的 snippet,用 {@render defaultButton()} 渲染,其余参数用于自建搜索界面。showSearch 为 false 时不渲染。TypeSnippet<[JsonViewerSearchButtonArgs]>-
showSearch是否显示搜索按钮。booleantrue
value编辑器的文本,配合 bind:value 使用;变为与编辑器当前内容不同的值时重建编辑器,与当前内容相同的回写不会丢失光标;未传时按空文本创建。string-
width宽度,数字按 px 处理,字符串原样使用。number | string400

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行高number20
autoWrap是否自动换行booleantrue
readOnly是否只读booleanfalse
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缩进大小number4
insertSpaces是否使用空格进行缩进booleantrue
eol换行符string'\n'

CompletionOptions

属性说明类型默认值
staticCompletions静态补全项。只在按文档结构已推断出候选项时追加在其后,推断不出任何候选项时不提供;与已有候选项同 label 的不重复添加。只读取每项的 label、insertText(缺省时用 label)、documentationCompletionItem[]-

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)
JsonViewer203.14kb51.23kb
MonacoEditor5102.0 KB1322.7 KB

渲染不同量级数据耗时

注:

  • 测试数据生成方式详情可查阅 url
  • 当数据量级超出50w行时,ReactMonacoEditor 默认关闭高亮等行为,数据对比不遵循单一变量原则
组件1k行5k行1w行10w行50w行100w行300w行
JsonViewer30.42ms30.66ms36.87ms52.73ms111.02ms178.81ms506.25ms
ReactMonacoEditor72.01ms73.76ms76.64ms97.89ms133.31ms202.79ms495.53ms
性能提升57.70%58.41%51.87%46.11%---