最近在业务中遇到了一个关于 多级下拉 需求,须要将后端树状数据显示在 textarea
上,同时 textArea
中也能对数据进行处理,转化为能进行多级选择树状数据。javascript
拿问卷星的多级下拉举个例子,以下图所示,用户能够在 textArea
框进行多级下拉的数据的编写,第一行表明标题,余下的每一行表明一个多级下拉框中各级的数据,各级数据之间使用 /
来进行分隔。css
数据编辑完成保存以后,咱们将树状数据用在移动端或者小程序端,这样就完成了一个多级下拉的组件。html
今天这篇文章就简单介绍一下这个工做流程,主要包括:前端
textarea
上展现的 value
值 ?textarea
中的数据 转化为 树状数据 ?npm
的组件?关于多级下拉的 数据展现 在这篇文章中不会作介绍,那么接下来咱们就开始发车。java
这个组件是使用 React Hooks + TypeScript
来实现,众所周知,Hooks
是 React
将来的趋势,同时 TypeScript
也是 JavaScript
将来的趋势,小弟恰好拿这个组件练练手。node
打包工具用的是 Rollup
,由于打包组件库的 Rollup
比 Webpack
更受欢迎,Webpack
更适合打包复杂的大型项目。关于 Webpack
的学习,你们能够参考笔者整理的 Webpack 学习文档。react
项目结构以下所示:webpack
. ├── node_modules // 第三方的依赖 ├── example // 开发时预览代码 ├── public // 放置静态资源文件夹 ├── src // 示例代码目录 ├── app.js // 测试项目 入口 js 文件 └── index.html // 测试项目 入口 html 文件 ├── yarn.lock // 测试项目 yarn lock 文件 └── package.json // 测试项目 依赖 ├── src // 组件源代码目录 ├── components // 轮子的目录 ├──textarea // 项目内部使用的一个 textarea 组件 ├── index.less // 组件核心代码样式文件 └── textarea.tsx // 组件核心代码 ├── types // typescripe 的接口定义 ├── utils // 工具函数目录 ├── assets // 静态资源目录 ├── index.tsx // 项目入口文件 └── index.less // 项目入口样式文件 ├── lib // 组件打包结果目录 ├── test // 测试文件夹 ├── typings // 放置项目全局 ts 申明文件目录 ├── .babelrc // babel 配置文件 ├── .eslintignore // eslintignore 配置文件 ├── .eslintrc.js // eslint 配置文件 ├── .gitignore // git上传时忽略的文件 ├── api-extractor.json // 用于将多个 ts 声明文件合成一个 ├── jest.config.js // 测试配置文件 ├── .npmignore // npm 上传忽略文件 ├── README.md ├── tsconfig.eslint.json // ts 的 eslint 文件 ├── tsconfig.json // ts 的配置文件 ├── rollup.config.js // rollup 打包配置文件 ├── yarn.lock // yarn lock 文件 └── package.json // 当前整一个项目的依赖
对于项目中除了源码之外的一些知识点,笔者就不细说了,好比Rollup
如何配置;api-extractor
如何将多个声明文件生成一个等等,你们能够自行查阅一波。
仓库地址在此:多级下拉 textarea 组件git
Hooks
骨架代码咱们使用的 React Hooks
来编写这个组件,通常编写组件以前咱们须要明确这个组件该支持哪些功能,即支持哪些 props
,在这个组件中暂时支持下面这些参数:github
<TreeTextArea treeTitle={title} // 多级下拉 标题数据 treeData={tree_value} // 树状数据 row={21} // textarea 的行数 showNumber // 是否展现左侧 textarea 数字 shouleGetTreeData // 是否开启 处理树数据的功能 delimiter='/' // 以什么符号切割 maxLevel={4} // 支持的最大级数 onChangeTreeData={ // 与 shouleGetTreeData 进行搭配使用,返回处理后的标题和树状数据 (treeTitle, treeData) => { console.log('---treeTitle---', treeTitle); console.log('---treeData---', treeData); } } defaultData={DEFAULT_TEXT} // 树状数据默认值 placeholder='请输入标题,例:省份/城市/区县/学校 浙江省/宁波市/江北区/学校1' />
咱们在 src/components/textarea.tsx
中进行相应代码的编写,其中包括接收相应的传入相应的 props
值、刚进入页面的时候去 初始化数据、监听数据变化、获取树状值 等操做:
const TreeTextArea = (props: Props): JSX.Element => { // 一系列 props 数据的接受 // ... const [__textAreaData, setTextAreaData] = useState(''); const [__flattenData, setFlattenData] = useState([]); // 数据初始化 useEffect(()=>{ if (isArray(__treeData) && isArray(__treeTitle)) { // ... const flattenData = flattenChainedData(__treeData); const textAreaData = getTextAreaData(flattenData, titles); setFlattenData(flattenData); setTextAreaData(textAreaData.join('\n')); } return ()=>{ // willUnMount } }, []) // 监听数据变化 const onChange = (data: any): void => {} // 设置默认值 const getDefaultData = (): void => {} // 获取树状值 const getTreeData = (e: any): void => { const { onChangeTreeData } = props; // ... if (onChangeTreeData) { onChangeTreeData(levelTitles, valueData); } } return ( <div className={styles.wrapper}> <NumberTextArea row={__row} value={__textAreaData} onChange={onChange} showNumber={__showNumber} placeholder={__placeholder} errCode={__errCode} errText={__errText} /> { // ... // 填充默认值、获取树状值 代码 } </div> ) }
咱们内部还封装了一个 NumberTextArea
,在这个组件中我增长了 左侧序号显示、错误显示 等逻辑,具体的代码就不贴上来了,你们有兴趣能够参考源码。
关于相关的 React Hooks
知识你们能够自行查阅相关资料学习,笔者在这里也不作介绍了。
接下来咱们来看一下组件中最核心的 多级下拉逻辑处理。
在总体骨架代码搭建好以后,咱们就只需关注 textarea
处理数据的逻辑就好了。
首先咱们在 utils
目录下新建 testData.ts
,模拟后端的 json
数据,以下图所示:
咱们先从编辑开始提及,假如后端给了咱们要渲染的标题以及多级下拉的树状数据:
接着咱们但愿经过一些处理将后端给的数据修改为 textarea
中能够展现的 value
值,相似于下面的字符串做为 value
值:
这里咱们须要作的是将 树状数据 进行 扁平化处理,给每一级的数据增长一个 title
属性,这即是咱们须要在 textarea
每一行中所要展现的数据,相似以下的数据:
咱们须要将每一级的数据的都扁平化出来。
但这里有一个问题好比 浙江省/宁波市 和 浙江省/宁波市/海曙区 这是两个不一样的数据,可是在 textarea
中其实不须要展现 浙江省/宁波市 这一个数据的,因此我在这里作了一个判断,若是这一个数据有孩子的话,就给他增长一个属性 hasChildren
,让咱们在获取 textarea
的数据的时候作一下过滤就好了,不展现有属性 hasChildren
的数据。
那么咱们如何来扁平化数据呢?其实只要对树状数据作一下递归处理就好了。
/** * 将后端的 树状结构 数据 扁平化 * @param {Array} data : 后端 tree_node 数据 */ export const flattenChainedData = (data: any) => { let arr = []; forEach(data, (item) => { const childrens = item.children; const rootObj = createNewObj(item, item.value); if (childrens) { rootObj.hasChildren = true; } arr.push(rootObj); if (childrens) { // 递归得到全部扁平的数据 const dataNew = getChildFlattenData(childrens, item.value, 1); arr = concat(arr, dataNew); } }); return arr; }; /** * 递归得到 扁平数组 * @param {*} data : 要处理的数组 * @param {*} title : 前几级 拼的 title * @param {*} level : 当前级数 */ const getChildFlattenData = (data, title, level) => { // 超过最大级数 if (level > MAX_LEVEL) return false; if (!data) return false; let arr = []; forEach(data, (item) => { const { children } = item; const rootObj = createNewObj(item, `${title}/${item.value}`); if (children) { rootObj.hasChildren = true; const childrenData = getChildFlattenData(children, `${title}/${item.value}`, level + 1); arr.push(rootObj); arr = concat(arr, childrenData); } else { arr.push(rootObj); } }); return arr; };
其中上面的 createNewObj
是为扁平数据新增 value/title 属性,返回新的对象,具体就不上代码了。
转化为扁平数据以后,咱们就能够将数据中的 title 属性拿出来,组成 textarea 所需的数据便可:
/** * 将 扁平数据 转化为 textarea 中 value * @param {Array} flattenData : 扁平化数据 * @param {String} titles : textarea 第一行的 title */ export const getTextAreaData = (flattenData, titles) => { const newData = filter(flattenData, (item) => { return !item.hasChildren && item.status !== 2; }); const arr = []; arr.push(titles); forEach(newData, (item) => { arr.push(item.title); }); return arr; };
其中咱们过滤了 hasChildren
为 true
,同时这里以 status = 2
表示删除的数据,也进行过滤, 这样咱们即可以获得以下图所示的一个 textarea 数组:
接着咱们将这些数组经过 \n
换行符 join
起来就是咱们所需的 textarea
的 value
值了。
这里是整个多级下拉逻辑中最核心的部分,咱们须要将用户修改过的数据,与原来的数据进行关联,生成新的树状数据。咱们分为四个步骤来说解这一个步骤。
咱们会建立一个 数据处理类 treeTextAreaDataHandle
,并在 constructor
构造函数中传入 扁平化数据、textarea
文本框的值 来初始化一个实例对象。以后咱们会在此类中完善咱们处理数据的一些属性和方法。
class treeTextAreaDataHandle { // 扁平化数组 private flattenData: FlattenDataObj[]; // textarea 框 文本值 private textAreaTexts: string; constructor(options: treeTextAreaData) { const { flattenData, textAreaTexts } = options; this.flattenData = flattenData; this.textAreaTexts = textAreaTexts; } }
第一步咱们会生成用户修改后的 textarea
初始映射数据,咱们会根据级数分别放在不一样的数组中,举个🌰:
咱们会根据用户最后输入完成的 textarea
值,来生成一组根据级数排布的对象数据,以下图:
如上面这张图会转化为以下面图中的数据,这个数据会是咱们进行接下去三步操做的关键:
这里须要注意的一个问题,有可能在某一级是有同名的值存在,这个时候咱们不能单纯的就把这两个值认为是同一个值,而要去比较他们的爸爸是不是同样的,以此类推,递归比较直到第一级,若是都是同样的话,那么他们才是同一个值,不然就是不一样的值。
举个简单例子,好比有两个数据: 浙江省/宁波市/海曙区 和 江苏省/无锡市/海曙区 这两个值,虽然第三级中的 海曙区 名字是相同的,可是他们是两个不一样的值,他们应该被分配到两个不一样的 id,而且各自的 parent_id 也不同。
接下来上代码,咱们新建一个实例方法 transDataFromText
,在这个方法中进行树状数据的转化,并获得最后的数据:
/** * 将 textarea 数据 转化为 后端所需的树状结构数据 * @param {Array} flattenData : 扁平数据 * @param {String} texts : textarea 的文本 */ public transDataFromText() { const texts = this.textAreaTexts; const arr = texts.split('\n'); // 去除标题 if (arr.length > 1) { arr.shift(); } // 赋值每一行文字为数组 this.textAreaArr = arr; // 解析 TextArea 数据 为 指定 层级映射数据 this.parserRootData(); // ... }
咱们在 parserRootData
这个方法中去生成修改后的初始映射:
/** * 将 textarea 数据 转化为 相应级数 的 数据 * @param {Array} textArr : textarea 的文本 转化的数组 * @param {Number} handleLevel : 要处理的级数 */ private parserRootData() { // 每一行的 textArea 值 const textArr = this.textAreaArr; // 最大级数 const handleLevel = this.MAX_LEVEL; // 以什么分隔符切割 const delimiter = this.delimiter; // 去重 每一级 textArea 值 const uniqueTextArr = uniq(textArr); // 映射数据存放对象 const namesArrObj: namesArrObj = {}; // 根据最大级数为每一级建立一个数组 for (let i = 1; i <= handleLevel; i++) { namesArrObj[`${ROOT_ARR_PREFIX}_${i}`] = []; } // 遍历 每一行的 textArea 值 forEach(uniqueTextArr, (item: string) => { // 切割 每一行 字符串,生成字符串 const itemArr = item.split(delimiter); // 根据最大级数往 namesArrObj 塞数据 for (let i = 1; i <= handleLevel; i++) { if ( !treeTextAreaDataHandle.sameParentNew(namesArrObj, itemArr, i) && itemArr[i - 1] ) { // 建立一个对应级数的对象,塞入对应的数组 const obj: parserItemObj = {}; obj.id = _id(); obj.value = itemArr[i - 1]; obj.level = i; // 获取当前的级数的值,爸爸的 id const parentId = treeTextAreaDataHandle.getParentIdNew( namesArrObj, itemArr, i ); obj.parent_id = parentId; namesArrObj[`${ROOT_ARR_PREFIX}_${i}`].push(obj); } } }); // 保存到对象的 rootArrObj 属性值中 this.rootArrObj = namesArrObj; }
上面最为关键的一个方法就是静态方法 sameParentNew
,做用是帮咱们递归判断 两个相同名称的值是否真的相同。其实原理也很简单,也是 递归 判断他们各自的爸爸是否相同。具体代码你们能够参考源码。
其次这里还有用到相似:
id
的方法:_id()
parent_id
的静态方法:getParentIdNew
到这里咱们第一步生成 初始映射 数据就完成了,接下来咱们就须要结合后端提供给咱们的扁平数据 flattenData 来填充已存在的数据,同时筛选出新增的数据。
这一步咱们须要将后端给咱们的数据 flattenData
与咱们的初始映射数据进行比对。填充存在数据的属性同时筛选出新增的数据,并给新增数据加上属性 new = true
,最后塞到对应的对象对应级数组中去 existNamesArrObj
和 addNamesArrObj
。
举个🌰
咱们新增了 浙江省/宁波市/高新区,咱们能够在新增数据中的第三级中找到 高新区,由于 浙江省 和 宁波市 已经存在,他们不会被添加到新增的数组中去,只会在已存在的对象中被找到,而且会用后端给的 id
替换掉咱们以前生成映射数据是生成的 id
,以下图:
存在数据:existNamesArrObj
新增数据:addNamesArrObj
这里咱们还要注意的一个点是,咱们在进行数据筛选以前,须要将后端给的数据flattenData
数据中加上一个属性root_id
,它的做用是帮咱们将 修改后数据 和以前 后端给的数据 进行关联,好比上面咱们新增 高新区 这个例子,他的爸爸是已经存在的,他的id
是已经存在的 36178,可是新增的高新区的parent_id
是咱们在映射数据时生成的,这两个确定不相等,咱们须要借助root_id
来将这两个数据联系起来。
接下来上代码,咱们将这一波处理放到 handleExistData
方法中,
/** * 填充已有的数据,并筛选出新增的数据 * @param {*} TextAreaData : parserRootData() 处理的数据 * @param {*} newFlattenData : 扁平化数据 * @param {Number} handleLevel : 要处理的级数 */ private handleExistData() { const namesArrObj = this.rootArrObj; const newFlattenData = this.flattenData; const handleLevel = this.MAX_LEVEL; // 存在的数据 const existNamesArrObj = {}; // 新增的数据 const addNamesArrObj = {}; for (let i = 1; i <= handleLevel; i++) { addNamesArrObj[`${ADD_ARR_PREFIX}_${i}`] = []; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`] = []; } // flatten 加上 parser 的 映射 id this.setMapIdForFlattenDataToRootData(); for (let i = 1; i <= handleLevel; i++) { // 获取出事映射相应级数的数据 const curNamesArr = namesArrObj[`${ROOT_ARR_PREFIX}_${i}`]; forEach(curNamesArr, (item) => { // 设立一个标志位 // 标志这一级的数据是否存在 let flag = false; // 映射数据的属性 const { value, parent_id, id } = item; // 新增数据 obj const addNewObj: addNewObj = { level: i, value, id, new: true, root_id: id, }; // 遍历比较后端数据 与 映射数据 的 `value` 和 `level` // 来肯定他们映射数据是否存在 // 存在就 forEach(newFlattenData, (val) => { if (value === val.value) { if (val.level === i) { // level 等于 1 if (val.level === 1 && val.parent_id === 0) { const obj = { ...val }; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`].push(obj); flag = true; } // level 大于 1 if (val.level !== 1 || val.parent_id !== 0) { if (this.isExistitem(val, parent_id, i)) { const obj = { ...val }; existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`].push(obj); flag = true; } } } } }); // 若是是新增数据 if (!flag) { // 塞入 addNamesArrObj addNamesArrObj[`${ADD_ARR_PREFIX}_${i}`].push(addNewObj); // 塞入 最新 扁平化数据 newFlattenData.push(addNewObj); } }); } // 将 existNamesArrObj 挂到类属性 existNamesArrObj 上 this.existNamesArrObj = existNamesArrObj; this.addNamesArrObj = addNamesArrObj; }
上面的方法中还用到了一个比较重要的方法 isExistitem
方法,判断当前级数据是否存在,其原理跟parserRootData
中用到的 sameParentNew
相似,也是去递归比较在他们的爸爸是不是相同的,直到找到不一样的爸爸或者到第一级为止,只不过这里面比较的是 初始映射数据 和 后端扁平数据。
还有一个方法就是咱们上面讲到的给 后端扁平数据 添加 root_id
的方法 setMapIdForFlattenDataToRootData
,具体代码笔者不贴了,你们有兴趣能够自行查看。
处理完已存在数据,和新增数据,咱们还须要处理删除的数据。
这里若是需求中要出对数据进行排序的话,其实能够吧 存在数据 和 新增数据 放在一个对象中,这样每次有新增或者存在数据的时候都会从上都下依次塞入,如今笔者是吧 存在数据 和 新增数据 分开来了, 新增数据 默认都是在最后的。
通常来讲,若是数据删除了,前端还须要将数据传给后端,告诉后端这条数据删除了。因此咱们须要给删除的数据中加上相应的 状态值,这里咱们加了 status = 2
,表明此条数据在前端已经被删除了。
实现起来很简单,由于咱们经过第二步已经获得了 已经存在的数据,只须要拿它与最初的 后端提供的扁平数据 进行比较一波就能得出哪些数据被删除了,筛选出来以后将他们将上相应的属性便可。
好比咱们删除了 江苏省/无锡市/惠山区 这一行,实际上是删除了 无锡市 和 惠山区 两个数据,咱们能够获得以下结果:
接下来上代码,咱们将筛选删除数据的方法写在 handleTagForDeleleByLevel
方法中,
/** * 根据标题 几级 来获取删除的数据,并给删除数据打上标签,并返回删除数据 * @param {*} handleDataArr : fillExistData() 处理的数据 * @param {*} newFlattenData : 扁平化数据 * @param {Number} handleLevel : 要处理的级数 */ private handleTagForDeleleByLevel = () => { const existNamesArrObj = this.existNamesArrObj; const handleLevel = this.MAX_LEVEL; // 存放 存在扁平数据 的数组 let existData = []; // 遍历 存在数据对象 扁平化存在数据 for (let i = 1; i <= handleLevel; i++) { const curArray = existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`]; existData = concat(existData, curArray); } // 给删除数据添加属性 status = 2 const deleteData = this.addTagForDeleleData(existData); // 将 deleteData 挂到属性 deleteData 上 this.deleteData = deleteData; };
咱们经过 addTagForDeleleData
方法来比较不一样的值,并加上属性 status=2
,在这里咱们也可使用lodash
的 difference
方法来获得两个数组不一样的值。
处理完上面三步以后,基本上就大公告成了,接下来就生成最终树状数据。
最后咱们就须要将 存在数据、新增数据、删除数据 生成一个新的扁平化数组,由这个新扁平化数据生成咱们想要的树状数据。
好比咱们新增 浙江省/宁波市/高新区,删除 江苏省/无锡市/惠山区,最终会获得新的扁平数据以下,咱们能够看到 高新区 是新增的,惠山区 也加上了相应的 status=2
的属性:
接着咱们就能够根据这个扁平化数据,递归生成树状数据,以下图:
接下来上代码,首先 getLastFlattenData
方法,经过这个方法咱们能够获取到最新的扁平化数据:
/** * 生成最新的数据 * @param {*} existNamesArrObj : existNamesArrObj 已存在数据 * @param {*} addNamesArrObj : addNamesArrObj 新增数据 * @param {*} deleteData : addTagForDeleleByLevel() 获得的删除数据 * @param {Number} handleLevel : 要处理的级数 */ private getLastFlattenData() { const existNamesArrObj = this.existNamesArrObj; const newAddNamesArrObj = this.newAddNamesArrObj; const deleteData = this.deleteData; const handleLevel = this.MAX_LEVEL; let lastData = []; let AddLast = []; let ExistLast = []; // 遍历 扁平化 存在和新增数据 for (let i = 1; i <= handleLevel; i++) { const curArrayExist = existNamesArrObj[`${EXIST_ARR_PREFIX}_${i}`]; const curArrayAdd = newAddNamesArrObj[`${HANDLE_ADD_ARR_PREFIX}_${i}`]; ExistLast = concat(ExistLast, curArrayExist); AddLast = concat(AddLast, curArrayAdd); } // 合并三种类型的数据 lastData = concat(lastData, ExistLast, AddLast, deleteData); // 将 lastData 挂到 类属性 newDataLists 上 this.newDataLists = lastData; };
最后就是生成最终树状数据,原理就是从 parent_id
为 0
开始进行 递归遍历,直到遍历完全部节点为止,与此同时咱们须要在生成树以前,删除一些本来不须要的属性,好比新增属性 new
,映射关联的 root_id
等。
/** * 删除 以前 组装 树状结构时 使用的 一些自定义属性 * 后端不须要 * @param {Object} item : 每一项的 item */ static clearParamsInTreeData = (item) => { delete item.title; delete item.hasChildren; delete item.root_id; if (item.new) { delete item.new; delete item.id; delete item.parent_id; } }; /** * 递归 将扁平数据转化为 树状 结构数据 * 用于 transDataFromText * @param {Array} lists : 扁平数据 * @param {Number} parent_id : 爸爸的 id */ private getTreeDataBylists = (parent_id: number | string): any => { const lists = this.newDataLists; //递归,菜单 const tree = []; forEach(lists, (item) => { const newItemId = item.parent_id; if (parent_id === newItemId) { const childrenTree = this.getTreeDataBylists(item.id); if (isArray(childrenTree) && childrenTree.length > 0) { item.children = childrenTree; } else { item.children = null; } // 删除没必要要属性 treeTextAreaDataHandle.clearParamsInTreeData(item); tree.push(item); } }); return tree; };
到这里咱们变完成了对 textarea
的处理,最终的 transDataFromText
方法以下:
/** * 将 textarea 数据 转化为 后端所需的树状结构数据 * @param {Array} flattenData : 扁平数据 * @param {String} texts : textarea 的文本 */ public transDataFromText() { const texts = this.textAreaTexts; const arr = texts.split('\n'); if (arr.length > 1) { arr.shift(); } this.textAreaArr = arr; // 解析 TextArea 数据 为 指定 层级映射数据 this.parserRootData(); // 填充已有数据 并 筛选新增数据 this.handleExistData(); // 处理新增数据 this.handleParamsInAddData(); // 获取删除数据 this.handleTagForDeleleByLevel(); // 获取最新扁平数据 this.getLastFlattenData(); // 获取最新树状数据 this.lastTreeData = this.getTreeDataBylists(0); return this.lastTreeData; }
咱们须要对一些错误进行处理,好比 用户可能不会输入标题、又或者 用户输入的标题大于了最大支持级数(固然在咱们项目中,这个最大支持级数用户能够本身来控制)、又或者 标题的级数与下面内容的级数不对应,这些都应该被归为错误列表中。
举个例子,当用户没有输入标题的时候,咱们应该提示其输入标题,以下图:
咱们新建一个方法 isEquelLevel
方法,来检测用户输入的值是否符合规范,代码其实也很简单,咱们能够取到最终的数据,遍历数据中是否存在错误,存在错误就抛出相应的 错误码 errorCode
和 错误信息 ERROR_INFO
,错误类型以下:
/** * 校验信息 */ export const ERROR_INFO = { 1: '第一行标题不可为空', 2: `第一行标题不可超过 ${MAX_LEVEL} 列`, 3: '标题和选择项的层级数请保持一致', 4: `选择项不可超过 ${MAX_LEVEL} 行`, 5: '请至少填写一行选择项', };
功能写完以后,咱们须要测试一下组件的功能,能够借助使用 create-react-app
的 react-scripts
帮咱们快速启动一个应用:
package.json
配置:如下是测试项目 package.json
文件:
{ "name": "example", "version": "0.0.0", "description": "", "license": "MIT", "private": true, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", }, "author": "Darrell", "dependencies": { "lodash": "^4.17.15", "react": "link:../node_modules/react", "react-dom": "link:../node_modules/react-dom", "react-scripts": "^3.4.1" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] }
这里面须要注意的一个问题就是,这里面的 react
和 react-dom
两个依赖须要使用上一级根目录 node_modules
下的依赖。
由于使用 Hooks
写的插件会由于有多个 React
应用而报错,以下图:
致使这个问题的缘由主要是第一个 React
版本没到 16.8
,或者第三个,在项目中有多个 React
引用。
至于第二个问题,Hooks
不符合规范基本上在咱们安装了 eslint-plugin-react-hooks
插件以后就基本上能够规避掉了。关于这个问题的更多信息你们能够参考 这条 issure。
而后咱们进入 exmaple
安装相应的依赖,直接运行 yarn start
就能够将咱们的项目跑起来了。
咱们在 example
的 public
目录下新建
index.html
:项目的模版文件,即负责项目显示的 html
文件<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <title>测试页面</title> </head> <body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id="root"></div> </body> </html>
manifest.json
:若是写手机端的 h5app
,图标、主题颜色等是在这个文件里是设置的,在这里咱们能够随意配置一波。同时在 src
目录下新建:
index.js
:项目入口文件index.css
:入口文件的样式import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App' ReactDOM.render(<App />, document.getElementById('root'))
App.js
:测试组件的文件import React, { Component } from 'react' // 测试数据 import { title, tree_value, DEFAULT_TEXT } from './testData' import TreeTextArea from 'darrell-tree-textarea' export default class App extends Component { render () { return ( <div className='App'> <TreeTextArea treeTitle={title} treeData={tree_value} row={21} showNumber shouleGetTreeData delimiter='/' maxLevel={4} onChangeTreeData={ (treeTitle, treeData) => { console.log('---treeTitle---', treeTitle); console.log('---treeData---', treeData); } } defaultData={DEFAULT_TEXT} placeholder='请输入标题,例:省份/城市/区县/学校 浙江省/宁波市/江北区/学校1' /> </div> ) } };
npm
以前的测试上面的测试文件是写在咱们的组件项目中的。
可是通常在发包以前,咱们须要在其余的项目里面测试使用一下,这个时候咱们能够借助 npm link
。
npm link
,这句命令意思就是将组件引入到全局的 node_modules
。npm link <package 名>
看个🌰:假设咱们使用 create-react-app
新建了一个项目 my-app
,咱们就能够在此项目的根目录下面,运行:
npm link @darrell/darrell-tree-textarea
这个时候咱们能够在项目中有了咱们项目的依赖:
可是由于是项目的引用,因此这个依赖包含了咱们插件项目中的全部内容,包括 node_modules
,这里会出现咱们上面提到的 Hooks
开发组件 Invalid hook call
这个错误,由于在咱们的依赖下有 @darrell/darrell-tree-textarea
下有 node_modules
文件下,在它下面有 React
依赖,同时在 my-app
下面的 node_modules
下也有 React
依赖,因此就会出现 多个 React
引用 这个问题。
这个问题在咱们发到 npm
上以后不会出现,由于在上传到 npm
上的时候是不会把 node_modules
目录传上去的。
解决办法有两个:
@darrell/darrell-tree-textarea
下的 node_modules
,可是每次都须要从新安装my-app
项目下,改一下配置文件,将全部的 react
引用指向同一个引用alias: { // ... 'react': path.resolve(__dirname, '../node_modules/react'), 'react-dom': path.resolve(__dirname, '../node_modules/react-dom'), },
关于如何发包你们能够参考这篇文章:从零开始实现类 antd 分页器(三):发布npm,这篇文章中有详细的介绍组件的测试和 npm
的发布,在本篇文章中就不涉及了。
本文主要讲了如何制做一个能处理 多级下拉树状数据 的 textarea
组件的编写,总体来看仍是比较简单,整个组件的难点应该是如何有效的 递归处理 数据:
children
。还有在组件测试那里也折腾了蛮久的,由于碰到了 React Hooks
组件不能运行的问题,我曾一度觉得是 Hooks
的写法有问题,后来没想到是多个 React
引用出现的错误。
不过如今回过头来思考这个问题,发现 React
的错误提醒其实作的很清楚,本身只要跟着这个错误提示一步一步就能把问题解决掉。
实不相瞒,想要个赞!