使用场景
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') => booleanvalue: 待匹配的值(JSON 键或值)。当match为函数时,value会尽量传入解析后的原始类型(number / boolean / null / string),因此可以使用===进行严格匹配;当match为字符串或正则时,仍基于文本内容匹配(字符串类型会去除两侧引号)path: 当前匹配到的路径,格式为root.key1.key2.key3[0].key4tokenType: 当前 token 的类型,'key'表示 JSON 键名,'value'表示 JSON 值。可用于区分同名键和值的匹配
name: 可选,规则名称,在#customRender插槽中用item.name区分规则#customRender插槽:作用域参数为item、index、contentcontent: 匹配到的内容。如果是字符串类型的值,将包含双引号(如"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 使用;变为与编辑器当前内容不同的值时重建编辑器,与当前内容相同的回写不会丢失光标;未传时按空文本创建。 | string | undefined |
| width | 宽度,数字按 px 处理,字符串原样使用。 | string | number | semiDefault("JsonViewer", "width", 400) |
| height | 高度,数字按 px 处理,字符串原样使用。 | string | number | semiDefault("JsonViewer", "height", 400) |
| showSearch | 是否显示搜索按钮。 | boolean | semiDefault("JsonViewer", "showSearch", true) |
| options | 编辑器配置,字段见 JsonViewerOptions。ariaLabel、readOnly、autoWrap 的默认值按键合并,只传部分字段时其余仍取默认值。内容按深比较变化时重建编辑器;就地修改 formatOptions、customRenderRule 等嵌套对象不会被察觉,需替换为新对象。 | JsonViewerOptions | semiGlobalDefault("JsonViewer", "options") |
| limitSearchButtonBounds | 是否限制搜索按钮的拖动范围在组件容器内。 | boolean | semiDefault("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 | 行高 | number | 20 |
| autoWrap | 是否自动换行 | boolean | true |
| readOnly | 是否只读 | boolean | false |
| 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 | 缩进大小 | number | 4 |
| insertSpaces | 是否使用空格进行缩进 | boolean | true |
| eol | 换行符 | string | '\n' |
CompletionOptions
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| staticCompletions | 静态补全项。只在按文档结构已推断出候选项时追加在其后,推断不出任何候选项时不提供;与已有候选项同 label 的不重复添加。只读取每项的 label、insertText(缺省时用 label)、documentation | CompletionItem[] | - |
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) |
|---|---|---|
| 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% | - | - | - |