用 TypeScript 写一个轻量级的 UI 框架之十四:Tree 树组件(上)

我们知道 JSON 是 JavaScript 里面的一大利器,特别是构建复杂的树状结构,简单不失强大。它可以基于两种数据类型构建下面两种多层嵌套的树:

  • 使用纯对象 Map 创建的树(需选定一个 id 作为 map 的 key,形成访问 path);
  • 综合使用 Array/Map 创建的树(无须访问 path,默认访问器为数组的索引);

可见对于不同类型的树,事前应该要确定清楚何种数据结构。通过 TypeScript 强大的类型系统,可以轻松地为我们去表述那些复杂的 JSON 结构。一旦确定好结构如何,剩下的解析、转换工作就简单多了。总的来说,解析树结构无非遍历工作。而遍历来说大概也不会太难,比较多使用到的是函数的递归而已。还有一点就是离不开栈(Stack)、队列(Queen)的应用。

对于这些所提到的事物,在面向对象编程中都要一一编码与之对应。简单说就是首先要“名词化”,然后勾勒出其内部结构,最后描述几个名词之间的关系如何。有点像数据库的 ER 建模,却又不尽相同。

这正好是发挥 TypeScript 强类型系统的好时机——“代码即文档”,能够清晰描述对象的数据结构并带有类型提示,——那不就是“文档”吗!?这肯定是 JavaScript 不能做到的事情,故所以我们要好好掌握和把握住这强大的优势。

配套源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-ts/src/list/tree

JSON Map 嵌套

TypeScript 中声明一个普通 JSON,就是 map 的 key/value:

export interface JsonMap {
	[key: string]: any;
}

any 类型太笼统了,不妨想想 JSON key/value 的值类型一般有哪些?我们约定好先,于是有下面类型。

/**
 * JSON 中 value 值可能出现的类型
 */
export type JsonValue = null | boolean | number | string;

于是 JsonMap 类型变成为:

export interface JsonMap {
	[key: string]: JsonValue;
}

也许你会补充,不是还有嵌套的类型吗?——是的,但是别急,嵌套啥我们还未交代呢。数组和 map 都可以嵌套下去。一开始为了简单点我们用 Map,那么就是 JsonMap 本身了。

export interface JsonMap {
	[key: string]: JsonValue | JsonMap;
}

没 JSON 例子,很难理解具体结构。所以我们下一节就讲。

例子一:配置模块的树结构

配置系统的存储结构也是一种树,典型如 Windows 系统的注册表,其实就是一颗巨大无比的树状结构。通过配置来说明树系统可以说是一个好的例子——还是比较简单的说。配置系统的树使用了纯对象 Map 来创建树,而不是数组,如下截图里面的 JSON 配置可见。

在这里插入图片描述

某一节点中的 Map 结构,其中 key 可以理解为 JSON 的路径 Path,不同层级的 Path 之间使用 . 隔开,若从顶级到末级排序,形成如 data.entityProfile.article.id 唯一访问路径。

节点(特定的类型)可以有下一级子节点,允许的数量是零到多个。都是 key/value,怎么识别那是节点呢?很简单,只要 value 是 {} 的说明就是节点,而非 numberbooleanstring 这些类型。细心的读者可能会发现,上例 data.entityProfile,清一色 value 为 {},是不是节点呢?虽然它更像是一个子级节点的容器,但它实质也是节点,因为以后不排除为这个节点增加更多的字段——这也是允许的。

配置模块的说明

前面说了一个例子,而 JSON 还有不同的情形来表示树。弄不好会搞得很混乱的。这里,有必要先交代一下配置 Config 的设计大概,我们假定如下。

  • 一个 JSON 文件,记录所有配置内容(如上图所示,即上一个例子)
  • 一个 JSON Scheme 文件,用于描述和规范上面 JSON 配置文件是具体什么配置,包含有:名称、说明、数据类型、渲染 UI 的类型等等的字段(如下图一所示)
  • JSON 后台模块程序,负责 JSON 文件的 CRUD
  • 让用户配置的前端界面程序(如下图二所示)

在这里插入图片描述
在这里插入图片描述
两份 JSON 文件通过一模一样的 key 匹配,一一对应的。

例子二:JSON Scheme

例子一的 value 类型仅限于 JsonValue 和 JsonMap,那是否允许有自定义类型呢?答案是肯定的。配置说明 JSON Scheme 正好就是这么一个例子。

节点之 value 是具体的配置内容,虽然也是用 map 表示但是可以为称为特定的类型。类型拥有其约定好的字段,比如当前的为idnamecatalogId 等等,都有其特定的含义。多个字段构成了配置实体本身。要注意的是,虽然大家都是写成 {} 的 map,但是我们赋予它们的意义不同,于是在 TypeScript 中定义一个 ConfigScheme 类型。

/**
 * 说明配置的节点
 */
interface ConfigScheme {
	/**
	 * 完整 JSONPath 路径
	 */
	id: string;

	/**
	 * 配置名称
	 */
	name: string;

	/**
	 * 配置说明
	 */
	tip?: string;

	/**
	 * 配置值的类型
	 */
	type?: "boolean" | "string" | "number";

	/**
	 * 渲染的控件类型
	 */
	ui?: "text" | "input_text" | "textarea" | "radio" | "htmlEditor";

	value: any;
}

为 JsonMap 增加自定义类型的声明,使用了泛型。嘿嘿,现在轮到 TypeScript 的泛型出场了,熟悉 Java 泛型的我毫无违和感。

 export interface JsonMap<T> {
      [key: string]: JsonValue | JsonMap<T> | T;
}

配置说明的 JSON Tree 正是一个实现:tree.JsonMap<ConfigScheme>;

Array/Map 创建的树

对比上一种纯对象 Map 创建树的结构,某种程度来讲 Array/Map 创建的树可能会更简单一点,因为 Map 方案需选定一个 id 作为 map 的 key,形成访问 path;而数组方案则默认就是数组的访问器:索引 index。Array/Map 创建的树 JSON 例子如下。

var map = {
        a : 1,
        b : 2,
        c : {
            c1: 1,
            c2: 2,
            children : [ {
                d : 3
            } ]
        }
};

特点如下。

  • 若有根节点,它是一个 map
  • 若无根节点,则是一个数组:map[];
  • 子节点容器为数组,而不是 map,且数组对应的 key,固定命名为 children
  • 这种结构不适合频繁访问的场合,例如配置,试想下,访问配置用数组 arr[2][1][3] 的话,简直会疯掉;
  • 适合什么呢?UI 展示,或者频繁的树节点的 CRUD。

为了方便存储和 CRUD,我们需要得知节点若干信息:父节点的 id = pid,level 所在的第几层等,于是创建 TreeNode 类型如下。

export interface TreeNode extends BaseObject {
    /**
     * 父节点的 id
     */
    pid: number;

    /**
     * 所在的第几层
     */
    level: number;

    /**
     * 子节点
     */
    children?: TreeNode| TreeNode[];
}

最后的 children 字段是可选的,若无表示没有下级节点。当为 TreeNode 类型时候表示采用纯 Map 的结构;当为 TreeNode[] 时候表示采用数组的结构(Array/Map 创建的树)。

从 DB 还原树

数据库 DB 里面存储都是如数组般的记录列表,体现不出树状的结构。我们称为下面的类型。

/**
 * 扁平的节点列表
 * 原始未处理的树节点
 */
export type FlatTreeNodeList = TreeNode[];

如何变成树呢?需要 aj.tree.toTreeArray() 方法。

/**
 * 生成树,将扁平化的数组结构 还原为树状的 Array结构
 * 父id 必须在子 id 之前,不然下面 findParent() 找不到后面的父节点,故先排序
 * 
 * @param jsonArray 
 */
export function toTreeArray(jsonArray: TreeNode[]): TreeNode[] {
    let arr: TreeNode[] = [];

    for (let i = 0, j = jsonArray.length; i < j; i++) {
        let n: TreeNode = jsonArray[i];

        if (n.pid === -1)
            arr.push(n);
        else {
            let parentNode: TreeNode | null = findParentInArray(arr, n.pid);

            if (parentNode) {
                if (!parentNode.children)
                    parentNode.children = [];

                (<TreeNode[]>parentNode.children).push(n);
            } else
                console.log('parent not found!');
        }
    }

    return arr;
}

注意父 id 必须在子 id 之前,不然下面 findParent() 找不到后面的父节点,故先排序。排序一般在后台搞定。

应用该结构的一个例子是树形菜单控件(访问在线例子):

在这里插入图片描述

固定 children 的纯 map 结构

这种也是使用 Map/Array 创建的树。与第一种纯 Map 树相比,不同在于:一、固定 children 作为下级节点容器,且为数组 Array;二、固定一个 id 作为 map 的 key,形成访问 path。

JSON 例如如下图。
在这里插入图片描述

因为使用了 id:number 作为 key,所以咋一看上去有点像第二种结构数组,实质不然,多个节点的容器为 map {},而不是 []

和数组相似也有 findParentInMap()/toTreeMap(),还提供一个遍历的方法 output()

let stack: TreeNode[] = [];

/**
 * 遍历各个元素,输出
 * 
 * @param map 
 * @param cb 
 */
export function output(map: TreeNode, cb: (node: TreeNode, id: string) => void): void {
    stack.push(map);

    for (let i in map) {
        map[i].level = stack.length;// 层数,也表示缩进多少个字符
        cb && cb(map[i], i);

        let c: TreeNode = <TreeNode>map[i].children;
        c && output(c, cb);
    }

    stack.pop();
}

利用它制作下拉菜单。其实数组也可以的,id 在实体里面也有,不用放在 map 的 key 上面。
在这里插入图片描述

小结

树结构了解好,剩下就是各种控件的开发。

相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页