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 setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';
</script>

基本用法

JsonViewer 的基本用法。传入 height 和 width 参数,设置组件的高度和宽度,通过 modelValue 传入 Json 字符串作为初始值。
编辑内容变化时触发 update:modelValue 与 change,载荷都是编辑器的当前文本,因此可以配合 v-model 使用。modelValue 变为与编辑器当前内容不同的值时才会重建编辑器,v-model 回写同一内容不会丢失光标;单向绑定 :modelValue 时,编辑器保留用户的输入,不会被拉回。若需要获取组件的值,也可以通过模板 ref 调用 getValue(),具体可参考 Methods。

代码
<script setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
</script>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="100" :modelValue="data" :width="700" />
    </div>
</template>

设置行高

配置 options 的 lineHeight 参数,设置固定行高(单位:px, 默认 20)。

代码
<script setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
    const wrapperStyle = { marginBottom: '12px', overflow: 'hidden' };
</script>

<template>
    <div>
        <div :style="wrapperStyle">
            <JsonViewer :height="100" :modelValue="data" :options="{ lineHeight: 20 }" :width="700" />
        </div>
        <div :style="wrapperStyle">
            <JsonViewer :height="120" :modelValue="data" :options="{ lineHeight: 24 }" :width="700" />
        </div>
        <div :style="wrapperStyle">
            <JsonViewer :height="120" :modelValue="data" :options="{ lineHeight: 26 }" :width="700" />
        </div>
    </div>
</template>

自动换行

配置 options 的 autoWrap 参数,设置为 true 时,组件会根据内容长度自动换行。

代码
<script setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    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>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="120" :modelValue="data" :options="options" :width="700" />
    </div>
</template>

格式化配置

配置 options 的 formatOptions 参数,设置组件的格式化配置。

  • tabSize: number,设置缩进大小为4,表示每级缩进 4 个空格
  • insertSpaces: boolean,true 表示使用空格进行缩进,false 表示使用制表符(Tab)
  • eol: string,设置换行符,可以是\n,\r\n,
代码
<script setup lang="ts">
    import { useTemplateRef } from 'vue';
    import { Button, JsonViewer, type JsonViewerHandle } from '@fibrous/semi-ui-vue';

    const data = `{
  "name": "Semi",
  "version": "0.0.0"
}`;
    const options = { formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' } };

    const jsonviewer = useTemplateRef<JsonViewerHandle>('jsonviewer');
</script>

<template>
    <div>
        <Button @click="jsonviewer?.format()">格式化</Button>
        <div :style="{ marginBottom: '16px', marginTop: '16px' }">
            <JsonViewer ref="jsonviewer" :height="100" :modelValue="data" :options="options" :width="700" />
        </div>
    </div>
</template>

自定义渲染规则

通过配置 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 插槽里区分规则。命中 token 的内容由 #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 插槽中用 item.name 区分规则
  • #customRender 插槽:作用域参数为 item、index、content
    • content: 匹配到的内容。如果是字符串类型的值,将包含双引号(如 "name","Semi")
    • 未提供插槽或插槽渲染为空时不做自定义渲染,保留 token 原文
代码
<script setup lang="ts">
    import { Image, JsonViewer, Popover, Rating, Tag, type JsonViewerOptions } from '@fibrous/semi-ui-vue';

    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 slot 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>

<template>
    <div>
        <div :style="{ marginBottom: '16px', marginTop: '16px' }">
            <JsonViewer :height="200" :modelValue="data" :options="options" :showSearch="false" :width="600">
                <template #customRender="{ item, content }">
                    <Popover v-if="item.name === 'popover'" showArrow trigger="hover">
                        <span>{{ content }}</span>
                        <template #content>我是用户自定义的渲染</template>
                    </Popover>
                    <Rating v-else-if="item.name === 'rating'" :defaultValue="Number(content)" disabled :size="10" />
                    <Tag v-else-if="item.name === 'tag'" shape="circle" size="small">{{ content }}</Tag>
                    <Popover v-else showArrow trigger="hover">
                        <span>{{ content }}</span>
                        <template #content>
                            <Image :height="100" :src="unquote(content)" :width="100" />
                        </template>
                    </Popover>
                </template>
            </JsonViewer>
        </div>
    </div>
</template>

自定义搜索按钮

通过 #searchButton 插槽,你可以自定义搜索按钮的渲染方式,实现固定位置、自定义样式等需求。提供插槽即整体替换默认按钮,defaultButton 是默认按钮的 VNode,用 <component :is> 渲染;showSearch 为 false 时两者都不渲染。

代码
<script setup lang="ts">
    import { IconSearch } from '@fibrous/semi-icons-vue';
    import { Button, JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
</script>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="200" :modelValue="data" :width="700">
            <template #searchButton="{ defaultButton, showSearchBar, onToggleSearchBar }">
                <div :style="{ position: 'absolute', top: '10px', right: '10px', zIndex: 10 }">
                    <Button v-if="!showSearchBar" @click="onToggleSearchBar">
                        <template #icon>
                            <IconSearch />
                        </template>
                        搜索
                    </Button>
                    <component :is="defaultButton" v-else />
                </div>
            </template>
        </JsonViewer>
    </div>
</template>

API 参考

JsonViewer

Props

属性说明类型默认值
modelValue编辑器的文本,配合 v-model 使用;变为与编辑器当前内容不同的值时重建编辑器,与当前内容相同的回写不会丢失光标;未传时按空文本创建。stringundefined
width宽度,数字按 px 处理,字符串原样使用。string | numbersemiDefault("JsonViewer", "width", 400)
height高度,数字按 px 处理,字符串原样使用。string | numbersemiDefault("JsonViewer", "height", 400)
showSearch是否显示搜索按钮。booleansemiDefault("JsonViewer", "showSearch", true)
options编辑器配置,字段见 JsonViewerOptions。ariaLabel、readOnly、autoWrap 的默认值按键合并,只传部分字段时其余仍取默认值。内容按深比较变化时重建编辑器;就地修改 formatOptions、customRenderRule 等嵌套对象不会被察觉,需替换为新对象。JsonViewerOptionssemiGlobalDefault("JsonViewer", "options")
limitSearchButtonBounds是否限制搜索按钮的拖动范围在组件容器内。booleansemiDefault("JsonViewer", "limitSearchButtonBounds", false)

Events

事件说明回调参数
update:modelValue编辑内容变化时触发,供 v-model 回写,参数为编辑器当前文本。[value: string]
change编辑内容变化时触发,参数为编辑器当前文本。它是组件事件,@change 不会收到搜索栏输入框冒泡上来的原生 change。[value: string]

Slots

插槽说明作用域参数
customRender替换被 options.customRenderRule 命中的 token 的内容,作用域参数为命中的规则 item、规则序号 index 与 token 原文 content;不提供或为空时保留原文。仅在只读模式下生效。JsonViewerCustomRenderSlotProps
searchButton整体替换默认搜索按钮;defaultButton 是默认按钮的 VNode,用 <component :is> 渲染,其余参数用于自建搜索界面。showSearch 为 false 时不渲染。JsonViewerSearchButtonSlotProps

Exposes

名称说明类型
getValue获取编辑器当前文本。Type() => string
format按 formatOptions 格式化当前内容。Type() => void
search搜索文本,可选参数控制大小写敏感、全词匹配和正则表达式。Type(searchText: string, caseSensitive?: boolean | undefined, wholeWord?: boolean | undefined, regex?: boolean | undefined) => void
getSearchResults获取当前搜索结果。Type() => FindMatch[] | null | undefined
prevSearch跳转到上一个搜索结果,可选步长。Type(step?: number | undefined) => void
nextSearch跳转到下一个搜索结果,可选步长。Type(step?: number | undefined) => void
replace替换当前搜索匹配项;替换文本为空串时删除匹配,没有匹配时不做任何事。Type(replaceText: string) => void
replaceAll替换所有搜索匹配项;替换文本为空串时删除全部匹配。Type(replaceText: string) => void
getStyle返回当前的 width 与 height。Type() => { width: string | number; height: string | number; }

class、style 与原生属性落在根元素上。根元素没有 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

Vue 中的类型名为 JsonViewerCustomRenderRule,规则只保存数据,命中 token 的内容由 #customRender 插槽绘制。

属性说明类型默认值
match匹配规则string | RegExp | (value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => boolean-
name规则名称,在 #customRender 插槽中区分规则string-

FormattingOptions

属性说明类型默认值
tabSize缩进大小number4
insertSpaces是否使用空格进行缩进booleantrue
eol换行符string'\n'

CompletionOptions

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

SearchControls

使用 #searchButton 插槽时,作用域参数(类型 JsonViewerSearchButtonSlotProps)除 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

可以通过模板 ref(类型 JsonViewerHandle)调用组件暴露的方法,实现某些特殊交互。

名称描述
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%---