(建议收藏)JS中5个处理树结构数据的常用方法及如何将其发布到npm上的教程讲解

1,370 阅读12分钟

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

前言

接着上篇实战系列文章说的,这次还是继续奉献,这次献出的是方法,而不是组件。顺便来说说如何编写一个typescript声明的树结构数据相关逻辑操作集的js库并发布npm包。没错,福利来了,这篇文章可以有效的解决你2个问题:

  1. 5个处理树结构数据的最优最简方法
  2. 如何发布一个使用typescript编写的js库到npm上?

在之前的这篇文章【实战:使用React+Typescript开发组件并发布到npm仓库】中有说到编写的组件如何发布到npm仓库上,如果还不清楚的小伙伴可以再去耐心看看,这次要说的发布js库相较于组件来说,要简单一点,毕竟不用涉及到webpack的相关知识,只需要你掌握typescript的些许知识即可。

其次就来一股脑地告诉你在我的项目中处理树结构数据的5大方法,保证你看完受益终生,建议你点赞收藏💓!

一、处理树结构数据的5大方法

这里展示的5大方法的代码逻辑其实没有什么难理解的,都是使用简单易读懂的代码实现的。所以下面的每一个方法就不去一行一行的讲解代码了。有一定代码功力的理解起来没多大问题,没有代码基础的需要就直接拷贝到项目中使用即可。(看过我文章的基本都是为了你们能直接拿过去使用的,所以这都不点赞收藏就说不过去了^_^)

代码实现全是使用的递归思想,如果不了解递归的,可以浏览一下这篇文章【炒冷饭系列5:了解一下Javascript中的递归?】。(杠精不要来杠用递归来实现这些方法有什么性能问题,性能那些都是后话,业务很着急你却写不出来这些方法的时候你才知道你是多么的可怜无助。)

废话不多扯,精彩在后头。接着往下看→

1.1 将树型结构数据转换成一维数组

/**
 * 将树型结构数据转换成一维数组
 * @param treeData
 */
const getListFromTree = (treeData: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {children: 'children', ...fieldNames};
    let tempList: any[] = [];
    treeData.forEach((v: any) => {
        tempList.push(v);
        let children = v[props.children];
        if (children && children.length > 0) {
            tempList = [...tempList, ...getListFromTree(children, props)];
        }
    });
    return tempList;
}

1.2 寻找指定子节点

/**
 * 寻找指定子节点
 * @param treeData
 * @param key
 * @param props
 */
const getNodeByKey = (treeData: any[], key: number | string, fieldNames?: FieldNames): any => {
    let props: any = {key: 'key', children: 'children', ...fieldNames};
    if (!treeData || treeData.length === 0) {
        return null;
    }
    for (let i = 0; i < treeData.length; i++) {
        let node: any = treeData[i];
        if (node[props.key] === key) {
            return node;
        }
        let children = node[props.children];
        if (children && children.length > 0) {
            let targetNode = getNodeByKey(children, key, props);
            if (targetNode) {
                return targetNode;
            }
        }
    }
    return null;
}

1.3 获取节点下的所有数据

/**
 * 获取节点下的所有子节点
 * @param treeData
 * @param key
 * @param props
 */
const getChildrenToList = (treeData: any[], key: number | string, fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', children: 'children', ...fieldNames};
    let targetNode: any = getNodeByKey(treeData, key, props);
    if (!targetNode) {
        return [];
    }
    let children = targetNode[props.children];
    if (children && children.length > 0) {
        return getListFromTree(children, props);
    }
    return [];
}

1.4 遍历每个树节点

/**
 * 遍历每个树节点
 * @param list
 * @param callback
 * @param props
 */
const forEachNode = (list: any[], callback: (v: any, list: any[]) => void, fieldNames?: FieldNames) => {
    let props: any = {children: 'children', ...fieldNames};
    list.forEach(v => {
        callback(v, list);
        let children: any = v[props.children];
        if (children && children.length > 0) {
            forEachNode(children, callback, props);
        }
    });
}

1.5 获取所有父节点

/**
 * 获取父级结点
 * @param key 目标节点的父节点的key
 * @param data  线性结构数据
 * @param props
 */
loopParentNodes = (key: string | number, data: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', parentKey: 'parentKey', children: 'children', ...fieldNames};
    let parentNodes: any[] = [];
    let target: any = data.filter(v => v[props.key] === key)[0];
    if (target) {
        parentNodes.push(target);
        parentNodes = [...parentNodes, ...loopParentNodes( target[props.parentKey], data, props)];
    }
    return parentNodes;
}


/**
 * 获取所有父节点
 * @param key
 * @param tree
 * @param props
 */
const getParentNodes = (key: string | number, tree: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', parentKey: 'parentKey', children: 'children', isTree: true, ...fieldNames};
    let data: any[] = props.isTree ? getListFromTree(tree, props) : tree;
    let targetNode: any = data.filter((v: any) => v[props.key] === key)[0];
    if (!targetNode) {
        return [];
    }
    let parentNodes: any[] = loopParentNodes(targetNode[props.parentKey], data, props);
    return parentNodes;
}

二、如何发布js库到npm仓库?

好了,上面的5大方法已经贴出来了,需要的自取就行,下面来聊如何把它发布到npm上的流程。请往下看→

2.1 开发

2.1.1 创建目录并初始化

使用以下命令生成一个treeUtils文件夹并对其进行初始化,如下:

mkdir treeUtils
cd treeUtils
npm init

在使用npm init命令时,会提示输入项目的名称、版本号、关键词、作者等,可以一路回车结束,后续也可对package.json文件进行修改。

这一步完成后,项目文件夹中就会新增一个package.json文件。接下来添加一些目录和文件,以下为我的目录结构:

├── dist  # 编译后的文件夹
     ├── treeUtils.d.ts 
     └── treeUtils.js 
├── example # 示例
       ├── test.ts 
            └── test.js
├── node_modules # 安装依赖时自动生成
├── .tsconfig # ts 配置
├── .gitignore # git 忽略
├── README.md
├── package-lock.json
└── package.json

package.json文件夹需要在main入口处填写编译后的入口文件,这里的入口文件为./dist/treeUtils.js,完整的package.json代码如下:

{
  "name": "tree-utils.js",
  "version": "0.0.3",
  "description": "树结构数据相关逻辑操作集",
  "main": "./dist/treeUtils.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Jacky010/tree-utils"
  },
  "keywords": [
    "tree",
    "utils",
    "typescript",
    "tree-utils.js"
  ],
  "author": "jacky010",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^4.7.4"
  }
}

2.1.2 tsc初始化并安装typescript

因为这个js库是使用typescript进行声明的,所以需要初始化tsc并对其进行配置。

tsc --init

执行上面的命令,将在文件中获得一个带注释的tsconfig.json文件,如果顺利你接着就可以对其进行如下配置:

"declaration": true, 
"outDir": "./dist",  // 设置为./dist

接着添加一个exclude,如下:

exclude: [
    "./dist",
    "./example"
]

最后得到完整的tsconfig.json配置代码如下:(为了不占字数将原文件的部分注释已去掉)

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
     "declaration": true,
     "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "exclude": [
    "./dist",
    "./example"
  ]
}

如果不顺利,那肯定会报tsc不是内部命令,那这时你就需要先在全局安装typescript,然后在通过命令tsc -v查看是否安装成功,成功之后才能进行tsc初始化。

npm install -g typescript
tsc -v

image.png

安装成功之后,细心的小伙伴通过上面给出的package.json代码可以发现依赖里面有安装typescript,也就是我们所写的这个js库用到的唯一依赖,也就是前言中提到的该篇文章主题之一:用typescript编写js库并发布。

安装typescript使用如下命令:

npm i typescript -D

2.1.3 编写代码

前期所需要的准备都已完成,接下来就是把上面分开讲解的操作树结构数据的5大方法组合起来成为一个完整代码,如下:

interface FieldNames {
    key?: string;   // 主键
    parentKey?: string; // 父节点主键
    children?: string;  // 子节点
    isTree?: boolean;   // 源数据是否是树结构
}


/**
 * 将树型结构数据转换成一维数组
 * @param treeData
 */
export const getListFromTree = (treeData: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {children: 'children', ...fieldNames};
    let tempList: any[] = [];
    treeData.forEach((v: any) => {
        tempList.push(v);
        let children = v[props.children];
        if (children && children.length > 0) {
            tempList = [...tempList, ...getListFromTree(children, props)];
        }
    });
    return tempList;
}


/**
 * 寻找指定子节点
 * @param treeData
 * @param key
 * @param props
 */
export const getNodeByKey = (treeData: any[], key: number | string, fieldNames?: FieldNames): any => {
    let props: any = {key: 'key', children: 'children', ...fieldNames};
    if (!treeData || treeData.length === 0) {
        return null;
    }
    for (let i = 0; i < treeData.length; i++) {
        let node: any = treeData[i];
        if (node[props.key] === key) {
            return node;
        }
        let children = node[props.children];
        if (children && children.length > 0) {
            let targetNode = getNodeByKey(children, key, props);
            if (targetNode) {
                return targetNode;
            }
        }
    }
    return null;
}


/**
 * 获取节点下的所有子节点
 * @param treeData
 * @param key
 * @param props
 */
export const getChildrenToList = (treeData: any[], key: number | string, fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', children: 'children', ...fieldNames};
    let targetNode: any = getNodeByKey(treeData, key, props);
    if (!targetNode) {
        return [];
    }
    let children = targetNode[props.children];
    if (children && children.length > 0) {
        return getListFromTree(children, props);
    }
    return [];
}


/**
 * 遍历每个树节点
 * @param list
 * @param callback
 * @param props
 */
export const forEachNode = (list: any[], callback: (v: any, list: any[]) => void, fieldNames?: FieldNames) => {
    let props: any = {children: 'children', ...fieldNames};
    list.forEach(v => {
        callback(v, list);
        let children: any = v[props.children];
        if (children && children.length > 0) {
            forEachNode(children, callback, props);
        }
    });
}


/**
 * 获取父级结点
 * @param key 目标节点的父节点的key
 * @param data  线性结构数据
 * @param props
 */
const loopParentNodes = (key: string | number, data: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', parentKey: 'parentKey', children: 'children', ...fieldNames};
    let parentNodes: any[] = [];
    let target: any = data.filter(v => v[props.key] === key)[0];
    if (target) {
        parentNodes.push(target);
        parentNodes = [...parentNodes, ...loopParentNodes( target[props.parentKey], data, props)];
    }
    return parentNodes;
}


/**
 * 获取所有父节点
 * @param key
 * @param tree
 * @param props
 */
export const getParentNodes = (key: string | number, tree: any[], fieldNames?: FieldNames): any[] => {
    let props: any = {key: 'key', parentKey: 'parentKey', children: 'children', isTree: true, ...fieldNames};
    let data: any[] = props.isTree ? getListFromTree(tree, props) : tree;
    let targetNode: any = data.filter((v: any) => v[props.key] === key)[0];
    if (!targetNode) {
        return [];
    }
    let parentNodes: any[] = loopParentNodes(targetNode[props.parentKey], data, props);
    return parentNodes;
}

2.1.4 编译文件并测试

代码编写完成,那现在就来对其进行编译。通过在cmd控制台输入命令tsc进行编译,成功之后会在文件生成一个dist文件夹,如下:

image.png

然后验证一下生成的treeUtils.js文件,看是否能引用成功。通过在cmd控制台输入命令cd dist进入dist文件夹,接着输入命令node,这时控制台会进入node编辑模式,然后在控制输入下图所示代码,结果可以看到上述代码编写的方法即证明该js库是可以使用的。

image.png

下面来验证其是否可以成功使用:

1、新建一个测试文件,在dist文件夹同级,新建一个example文件夹,里面新建test.ts文件,如下图所示:

image.png

import {
    getListFromTree,
    getNodeByKey,
    getChildrenToList,
    forEachNode,
    getParentNodes
} from '../dist/treeUtils';

const treeData: any[] = [
    {
        title: '0-0',
        key: '0-0',
        parentKey: '',
        children: [
            {
                title: '0-0-0',
                key: '0-0-0',
                parentKey: '0-0',
                children: [
                    { title: '0-0-0-0', key: '0-0-0-0', parentKey: '0-0-0' },
                    { title: '0-0-0-1', key: '0-0-0-1', parentKey: '0-0-0' },
                    { title: '0-0-0-2', key: '0-0-0-2', parentKey: '0-0-0' },
                ],
            },
            {
                title: '0-0-1',
                key: '0-0-1',
                parentKey: '0-0',
                children: [
                    { title: '0-0-1-0', key: '0-0-1-0', parentKey: '0-0-1' },
                    { title: '0-0-1-1', key: '0-0-1-1', parentKey: '0-0-1' },
                    { title: '0-0-1-2', key: '0-0-1-2', parentKey: '0-0-1' },
                ],
            },
            {
                title: '0-0-2',
                key: '0-0-2',
                parentKey: '0-0',
                children: null
            },
        ],
    },
    {
        title: '0-1',
        key: '0-1',
        parentKey: '',
        children: [
            { title: '0-1-0-0', key: '0-1-0-0', parentKey: '0-1' },
            { title: '0-1-0-1', key: '0-1-0-1', parentKey: '0-1' },
            { title: '0-1-0-2', key: '0-1-0-2', parentKey: '0-1' },
        ],
    },
    {
        title: '0-2',
        key: '0-2',
        parentKey: '',
        children: null
    },
];

console.log('getListFromTree', getListFromTree(treeData))
console.log('getNodeByKey', getNodeByKey(treeData, '0-0-0'))
console.log('getChildrenToList', getChildrenToList(treeData, '0-1'))
console.log(forEachNode(treeData, (v: any, list: any) => {console.log('子项:', v)}))
console.log('getParentNodes', getParentNodes('0-0-1-0', treeData))

2、编译test.ts文件

cd example
tsc test.ts

3、查看生成的test.js文件

image.png

4、node执行一下test.js文件,若看到下面截图则证明该js库没毛病,可行。

image.png

好了,代码编写完成,测试也通过那就可以去进行发布了,详细请往下看→

2.2 发布

若是在npm官网没注册过帐号,先注册一下,再来考虑发布包的问题,没有账号一切都是大空话。

2.2.1 登录

进入treeUtils文件夹,打开cmd命令弹窗,输入以下命令:

npm login

按照提示一次输入用户名、密码和邮箱(现在会发一条验证码邮件到你邮箱,然后要求你输入验证码),全部正确即可登录成功。

然后输入以下命令皆可以发布包了,如下:

npm publish

出现下面截图,即代表着你的包已成功发布

image.png

2.2.2 问题

发布的时候可能会出现以下问题(均是本人实践中出现并记录的):

  1. 若是遇到npm ERR! no_perms Private mode enable, only admin can publish this module: 这个错误,尝试下面解决的方法:
npm config set registry http://registry.npmjs.org

这个问题的产生的原因是由于本人公司开发时使用的是自己的私有服务,所以登录发布的时候需要切换到npmjs的网址即可。

  1. 若是遇到npm ERR! code E403 You do not have permission to publish "npmtest". Are you logged in as the correct user? 这个错误,主要是由于所要发布包的namenpmjs网上已经发布的包的名字重复,所以收你没有权限发布这个名字的包。

解决方法:找到package.json文件,把name的值换掉。如果还出现上述错误就是还是重名的,继续换!(本来我这个包开始的名字打算叫tree-utils的,结果报这个错,然后去npm上看发现已经被人使用了,所以不得已才换为tree-utils.js)。

  1. 若是遇到npm ERR! 403 403 Forbidden - PUT registry.npmjs.org/tree-utils.… - You cannot publish over the previously published versions: 0.0.2. 这个错误,主要原因是发布的包的版本需要往上升一个版本号

解决方法:找到package.json文件,把version的值上调一个版本即可。

2.3 使用

2.3.1 下载安装

该组件已经发布到npm上了,所以小伙伴们可以到npm上查看并下载,也可以通过如下命令进行安装使用:

npm i tree-utils.js 或 yarn add tree-utils.js

2.3.2 项目中引用

安装好之后,在项目中引用使用:

import {
  getListFromTree,
  getNodeByKey,
  getChildrenToList,
  forEachNode,
  getParentNodes
} from 'tree-utils.js';
const treeData: any[] = [
  {
    title: '0-0',
    key: '0-0',
    parentKey: '',
    children: [
      {
        title: '0-0-0',
        key: '0-0-0',
        parentKey: '0-0',
        children: [
          { title: '0-0-0-0', key: '0-0-0-0', parentKey: '0-0-0' },
          { title: '0-0-0-1', key: '0-0-0-1', parentKey: '0-0-0' },
          { title: '0-0-0-2', key: '0-0-0-2', parentKey: '0-0-0' },
        ],
      },
      {
        title: '0-0-1',
        key: '0-0-1',
        parentKey: '0-0',
        children: [
          { title: '0-0-1-0', key: '0-0-1-0', parentKey: '0-0-1' },
          { title: '0-0-1-1', key: '0-0-1-1', parentKey: '0-0-1' },
          { title: '0-0-1-2', key: '0-0-1-2', parentKey: '0-0-1' },
        ],
      },
      {
        title: '0-0-2',
        key: '0-0-2',
        parentKey: '0-0',
        children: null
      },
    ],
  },
  {
    title: '0-1',
    key: '0-1',
    parentKey: '',
    children: [
      { title: '0-1-0-0', key: '0-1-0-0', parentKey: '0-1' },
      { title: '0-1-0-1', key: '0-1-0-1', parentKey: '0-1' },
      { title: '0-1-0-2', key: '0-1-0-2', parentKey: '0-1' },
    ],
  },
  {
    title: '0-2',
    key: '0-2',
    parentKey: '',
    children: null
  },
];
useEffect(() => {
  console.log('getListFromTree', getListFromTree(treeData))
  console.log('getNodeByKey', getNodeByKey(treeData, '0-0-0'))
  console.log('getChildrenToList', getChildrenToList(treeData, '0-1'))
  console.log(forEachNode(treeData, (v: any, list: any) => {console.log('子项:', v)}))
  console.log('getParentNodes', getParentNodes('0-0-1-0', treeData))
}, [])

2.3.3 测试

如果你是在Githubclone下来的项目,想试着把项目跑起来看测试结果,可以通过以下操作即可达到目的:

首先进入tree-utils文件夹并执行命令npm install安装包,项目中只使用了typescript,所以第一步就是安装它;

然后依次执行以下命令:

cd example

tsc test.ts 

node test.js

接着你就可以看到和下图所示的图,这时就代表着你执行成功,该测试是通过的。

image.png

最后,这篇文章要聊的主题就聊完了。如果你还纠结树结构的数据不能正确处理,那就仔细看看上面的5大方法;如果你也想写一个公用的js包并发布,那你赶紧参考一下这篇文章,我相信如果认真读完这篇你一定会掌握树结构的数据的处理方法,也一定能成功地把自己编写的js包发布成功到npm上的,赶紧进来码住并按着方法一步步去实现吧。

资源

npm地址tree-utils.js

github仓库tree-utils

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

src=http___p6.itc.cn_q_70_images03_20210104_70f8545500034a5bae5f1695a7ce3da0.jpeg&refer=http___p6.itc.webp