我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
前言
接着上篇实战系列文章说的,这次还是继续奉献,这次献出的是方法,而不是组件。顺便来说说如何编写一个typescript
声明的树结构数据相关逻辑操作集的js
库并发布npm
包。没错,福利来了,这篇文章可以有效的解决你2
个问题:
5
个处理树结构数据的最优最简方法- 如何发布一个使用
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
安装成功之后,细心的小伙伴通过上面给出的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
文件夹,如下:
然后验证一下生成的treeUtils.js
文件,看是否能引用成功。通过在cmd
控制台输入命令cd dist
进入dist
文件夹,接着输入命令node
,这时控制台会进入node
编辑模式,然后在控制输入下图所示代码,结果可以看到上述代码编写的方法即证明该js
库是可以使用的。
下面来验证其是否可以成功使用:
1、新建一个测试文件,在dist
文件夹同级,新建一个example
文件夹,里面新建test.ts
文件,如下图所示:
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
文件
4、node
执行一下test.js
文件,若看到下面截图则证明该js
库没毛病,可行。
好了,代码编写完成,测试也通过那就可以去进行发布了,详细请往下看→
2.2 发布
若是在npm
官网没注册过帐号,先注册一下,再来考虑发布包的问题,没有账号一切都是大空话。
2.2.1 登录
进入treeUtils
文件夹,打开cmd
命令弹窗,输入以下命令:
npm login
按照提示一次输入用户名、密码和邮箱(现在会发一条验证码邮件到你邮箱,然后要求你输入验证码),全部正确即可登录成功。
然后输入以下命令皆可以发布包了,如下:
npm publish
出现下面截图,即代表着你的包已成功发布
2.2.2 问题
发布的时候可能会出现以下问题(均是本人实践中出现并记录的):
- 若是遇到npm ERR! no_perms Private mode enable, only admin can publish this module: 这个错误,尝试下面解决的方法:
npm config set registry http://registry.npmjs.org
这个问题的产生的原因是由于本人公司开发时使用的是自己的私有服务,所以登录发布的时候需要切换到npmjs
的网址即可。
- 若是遇到npm ERR! code E403 You do not have permission to publish "npmtest". Are you logged in as the correct user? 这个错误,主要是由于所要发布包的
name
和npmjs
网上已经发布的包的名字重复,所以收你没有权限发布这个名字的包。
解决方法:找到package.json
文件,把name
的值换掉。如果还出现上述错误就是还是重名的,继续换!(本来我这个包开始的名字打算叫tree-utils
的,结果报这个错,然后去npm上看发现已经被人使用了,所以不得已才换为tree-utils.js
)。
- 若是遇到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 测试
如果你是在Github上clone
下来的项目,想试着把项目跑起来看测试结果,可以通过以下操作即可达到目的:
首先进入tree-utils文件夹并执行命令npm install
安装包,项目中只使用了typescript
,所以第一步就是安装它;
然后依次执行以下命令:
cd example
tsc test.ts
node test.js
接着你就可以看到和下图所示的图,这时就代表着你执行成功,该测试是通过的。
最后,这篇文章要聊的主题就聊完了。如果你还纠结树结构的数据不能正确处理,那就仔细看看上面的5
大方法;如果你也想写一个公用的js
包并发布,那你赶紧参考一下这篇文章,我相信如果认真读完这篇你一定会掌握树结构的数据的处理方法,也一定能成功地把自己编写的js
包发布成功到npm
上的,赶紧进来码住并按着方法一步步去实现吧。
资源
npm地址:tree-utils.js
github仓库:tree-utils
往期精彩文章
- (建议收藏)React项目中文字展开收起组件的实现过程
- (建议收藏)React项目中实现文本宽度自适应组件的过程
- (建议收藏)使用React+Typescript开发组件并发布到npm仓库
- 如何在React项目中封装定制的筛选组件?
- GitHub Copilot体验:你的人工智能结对程序员来啦!
- Go语言系列:Go从哪里来,Go将去哪里?
后语
伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。