从0-1
实现一个G Json View
组件。
- 第一版
扁平化处理后渲染 G Json View 【一】 - 掘金 (juejin.cn)。
- 第二版
递归渲染。
第二版
递归渲染。
Vue3 JSX
写法递归实现。
主要实现思路
根据传入的json数据,判断数据类型。
-
数组
- 渲染数组的开始
- 递归渲染数组内部结构
- 渲染数组的结束
-
对象
- 渲染对象的开始
- 递归渲染对象内部结构
- 渲染对象的结束
-
内容
- 渲染内容
工具方法
用到的工具方法。
export function getType(value: any) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
export function isObject(value: any) {
return getType(value) === 'object';
}
export function isArray(value: any) {
return getType(value) === 'array';
}
主要代码
import { computed, defineComponent, reactive, watchEffect, } from 'vue';
import { isArray, isObject, getType } from "@/utils/utils";
import "./styles.less";
export default defineComponent({
name: 'TreeWrap',
props: {
json: {
required: true,
type: Object,
default: () => ({}),
},
},
setup(props, { emit, slots }) {
const state = reactive({
});
const renderItem = ({
key,
data,
level,
showComma,
}: any) => {
return (
<div
class="item-wrap"
style={{
marginLeft: `${level}em`,
}}
>
{key && <span class="node-key">{`${key}:${' '}${' '}`}</span>}
<span
class={[
'node-content',
['{', '}', '[', ']'].includes(data) ? '' : getType(data)
]}
>
{`${data}`}
</span>
{showComma && <span>{','}</span>}
</div>
);
}
const renderContent = ({
// 键值
key = null,
// json数据
data,
// 层级
level = 0,
// 是否显示逗号
showComma = false,
}: any) => {
if (isArray(data)) {
return (
<div
class="content-wrap"
style={{
// marginLeft: `${level}em`,
}}
>
{
renderItem({
key,
data: '[',
level,
showComma: false
})
}
{
data.map((item: any, index: number) => {
return renderContent({
key,
data: item,
level: level + 1,
showComma: index !== data.length - 1
});
})
}
{
renderItem({
key: null,
data: ']',
level,
showComma,
})
}
</div>
)
}
if (isObject(data)) {
return (
<div
class="content-wrap"
style={{
// marginLeft: `${level}em`,
}}
>
{
renderItem({
key,
data: '{',
level,
showComma: false
})
}
{
Object.keys(data).map((key: any, index: number) => {
return renderContent({
key,
data: data[key],
level: level + 1,
showComma: index !== Object.keys(data).length - 1
});
})
}
{
renderItem({
key: null,
data: '}',
level,
showComma,
})
}
</div>
);
}
return renderItem({
key,
data,
level,
showComma,
});
}
return () => {
return (
<div class='tree-node-wrap'>
{
renderContent({data: props.json})
}
</div>
)
};
},
});
.tree-node-wrap {
.node-index {
display: inline-block;
width: 1em;
color: rgb(88, 110, 117);
}
.icon-wrap {
cursor: pointer;
display: inline-block;
svg {
vertical-align: middle;
color: rgb(88, 110, 117);
height: 1em;
width: 1em;
}
}
.node-key {
color: #8c6325;
}
.node-content {
color: #000;
}
.node-content.string {
color: #57b73b;
}
.node-content.number {
color: #2d8cf0;
}
.node-content.boolean {
color: #1d8ce0;
}
.node-content.null {
color: #D55FDE;
}
.node-content.undefined {
color: #D55FDE;
}
}
以上基本就可以实现一个json的渲染展示了,后续主要就是一些优化和细节的处理。
优化和细节的处理
以下修改均在 `src/components/TreeWrap/TreeWrap.tsx``
- 层级样式优化(缩进和边框)
- 是否显示逗号
- 添加Icon
增加点击事件
- 绑定事件
- 点击后记录点击的位置,将点击范围内渲染替换为折叠样式
- 折叠时展示条数
增加配置项
- 深度,大于等于该深度的节点将被折叠。
props: {
...
deep: {
type: Number,
default: 4
},
}
setup(props, { emit, slots }) {
const pathOutDeep = (path: string) => {
if (props.deep) {
return path.split('[').length >= props.deep
}
return false
}
const renderContent = ({
...
}: any) => {
const isClosed = state.closedPath[path] === undefined ? pathOutDeep(path) : state.closedPath[path]
...
}
}
- 在数据折叠的时候展示长度
props: {
...
showLength: {
type: Boolean,
default: true
},
}
setup(props, { emit, slots }) {
const renderItem = ({
...
}: NodeDataType) => {
return (
<div
class={['item-wrap', level > 0 && 'need-indent', canClick && 'item-wrap-click']}
onClick={() => canClick && handleIconClick(isClosed, path)}
>
...
{props.showLength && !!itemsLen && <span class="items-length">{`${itemsLen} items`}</span>}
...
</div>
)
}
}
- 展示标识线
props: {
...
showLine: {
type: Boolean,
default: true
},
}
setup(props, { emit, slots }) {
const renderContent = ({
...
}: any) => {
...
if (isArray(data)) {
...
return (
<div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
...
</div>
)
}
if (isObject(data)) {
...
return (
<div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
...
</div>
)
}
return (
<div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
...
</div>
)
}
}
- 展示图标
props: {
...
showIcon: {
type: Boolean,
default: true
},
}
setup(props, { emit, slots }) {
const renderContent = ({
...
}: any) => {
...
if (isArray(data)) {
...
return (
<div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
{props.showIcon && renderIcon(isClosed, path)}
...
</div>
)
}
if (isObject(data)) {
...
return (
<div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
{props.showIcon && renderIcon(isClosed, path)}
...
</div>
)
}
}
}
- 展示 key 名的双引号
props: {
...
showDoubleQuotes: {
type: Boolean,
default: true
},
}
setup(props, { emit, slots }) {
const defaultKey = (key: string) => (
<span class={['node-key']}>
{`${props.showDoubleQuotes && '"'}${key}${props.showDoubleQuotes && '"'}:${' '}${' '}`}
</span>
)
const defaultValue = (value: any) => {
if (value === null) {
value = 'null';
} else if (value === undefined) {
value = 'undefined';
}
return getType(value) === 'string' ? `${isBracket(value) ? '' : props.showDoubleQuotes && '"'}${value}${isBracket(value) ? '' : props.showDoubleQuotes && '"'}` : value + '';
}
}
- 定义最顶层数据路径
props: {
...
rootPath: {
type: String,
default: 'root'
},
}
setup(props, { emit, slots }) {
const renderContent = ({
...
// 路径
path = props.rootPath || 'root'
}: any) => {
...
}
}
- 支持点击括号或文字折叠
props: {
...
collapsedOnClickBrackets: {
type: Boolean,
default: true
},
}
setup(props, { emit, slots }) {
const renderItem = ({
...
}: NodeDataType) => {
return (
<div
class={['item-wrap', level > 0 && 'need-indent', props.collapsedOnClickBrackets && canClick && 'item-wrap-click']}
onClick={() => props.collapsedOnClickBrackets && canClick && handleIconClick(isClosed, path)}
>
...
</div>
)
}
}
- 自定义渲染节点键
props: {
...
renderNodeKey: {
type: Function as PropType<
(opt: { node: NodeDataType; defaultKey: string | JSX.Element }) => unknown
>
},
},
setup(props, { emit, slots }) {
...
const defaultKey = (key: string) => (
<span class={['node-key']}>
{`${props.showDoubleQuotes && '"'}${key}${props.showDoubleQuotes && '"'}:${' '}${' '}`}
</span>
)
const renderKey = ({
key,
data,
level,
showComma,
path,
itemsLen,
isClosed,
canClick
}: NodeDataType) => {
if (props.renderNodeKey) {
return props.renderNodeKey({
node: {
key,
data,
level,
showComma,
path,
itemsLen,
isClosed,
canClick
},
defaultKey: defaultKey(key)
})
}
return defaultKey(key);
}
}
- 自定义渲染节点值
props: {
...
renderNodeValue: {
type: Function as PropType<
(opt: { node: NodeDataType; defaultValue: string | JSX.Element }) => unknown
>
}
},
setup(props, { emit, slots }) {
...
const defaultValue = (value: any) => {
if (value === null) {
value = 'null';
} else if (value === undefined) {
value = 'undefined';
}
return getType(value) === 'string' ? `${isBracket(value) ? '' : props.showDoubleQuotes && '"'}${value}${isBracket(value) ? '' : props.showDoubleQuotes && '"'}` : value + '';
}
const renderValue = ({
key,
data,
level,
showComma,
path,
itemsLen,
isClosed,
canClick
}: NodeDataType) => {
if (props.renderNodeValue) {
return props.renderNodeValue({
node: {
key,
data,
level,
showComma,
path,
itemsLen,
isClosed,
canClick
},
defaultValue: defaultValue(data)
})
}
return defaultValue(data);
}
}
- 点击节点时触发
props: {
...
nodeClick: {
type: Function as PropType<
(opt: { isClosed: Boolean, path: String }) => unknown
>
}
},
setup(props, { emit, slots }) {
...
const handleIconClick = (isClosed: boolean, path: string) => {
props.nodeClick && props.nodeClick({isClosed, path});
...
}
}
此方式不好加序号。
完整代码
Github:G Json View。