前言:Hello hello,还是那个野生前端自学React,自学真的是很艰难的旅程啊,从掘金拿走的知识,在这里还给掘金,虽然依然是个菜鸟,有一颗想火的心,轻点骂QAQ。
正文:那么还是那个那么,这次分析一下我模仿ant design的Menu组件,实现方式上有点区别,绝对没人家的好,没办法,源码刺伤了我的眼,知识也很局限。我把这个功能封装成了四个组件分别是
- Menu.js
- Group.js
- SubItem.js
- Item.js
1. 第一步简单分析一下,想要实现多级嵌套那难免用递归了,那组件之间的通讯我选择了上一次讲到的Context。难点有哪些:i.嵌套过程中padding的计算,ii.通过默认值定位到选中的item,iii.选中后逐级选中上一层的sub(有的情况下)。先看下最终产物,暂时不是个完成的站点,但不影响体验,使用代码如下。
<Menu>
<SubItem title="SubItem 1" key="sub1">
<Item key="sub1-1">Option 1</Item>
<Item key="sub1-2">Option 2</Item>
</SubItem>
<SubItem title="SubItem 2" key="sub2">
<Item key="sub2-1">Option 3</Item>
<Item key="sub2-2">Option 4</Item>
</SubItem>
<SubItem title="SubItem 3" key="sub3">
<Item key="sub3-1">Option 1</Item>
<Item key="sub3-2">Option 2</Item>
<SubItem title="SubItem 4" key="sub4">
<Item key="sub4-1">Option 1</Item>
<Item key="sub4-2">Option 2</Item>
</SubItem>
</SubItem>
</Menu>
2. 那么我们一步步来,先创建Menu.js
使用Context来实现数据共享,分别共享了默认选中和默认展开哪些父标签,以及它们的修改函数都是用useState注册的,还有一个uniqueOpened参数表示是否只允许展开一项。介绍完共享数据,Menu组件内容还是比较简单的,仅仅是遍历传入的children组件并展示。但是这里有个细节,不是单纯返回子组件,而是通过cloneElement这个api克隆了一遍,这个api主要可以给嵌套的子组件添加属性,因为子组件的属性在用户那就定好了,我们无法添加,这就是这个api的功能。那么我准备添加的是tkey(key的备份,它可以被访问),因为React中key不可读的属性,又不能让用户多传一个key。然后还添加了一个index,主要作用是分清楚层级,已确定对应的padding-left该设置多少(这里就是第一个难点的关键点)。最后一个属性unitKey,它是用来处理第三个难点的,将每一条路线(路线指树形结构里的线路)连起来,比如1节点是a,那么1节点下个点会连上a,变成a-b或是a-c等等,建立一种关系。有点长,这就是Mneu.js的所有了,在下手残10级,很抱歉。
import React from 'react';
import {useState} from 'react';
import style from './Menu.less';
import {TestContext} from './Context';
function cloneChild(child) {
return React.cloneElement(child, {index: 1, tkey: child.key, unitKey: child.key});
}
function Menu(props) {
const [defaultOpenKeys, setDefaultOpenKeys] = useState(props.defaultOpenKeys || []); // must Array
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(props.defaultSelectedKeys);
return (
<TestContext.Provider
value={{
defaultOpenKeys: defaultOpenKeys,
setDefaultOpenKeys: setDefaultOpenKeys,
defaultSelectedKeys: defaultSelectedKeys,
setDefaultSelectedKeys: setDefaultSelectedKeys,
uniqueOpened: props['unique-opened']
}}
>
<div className={style.menu}>
{props.children.map(item => {
return cloneChild(item);
})}
</div>
</TestContext.Provider>
)
}
export default Menu
3. 讲完了Menu组件,我们来聊聊Group.js组件,它就会比较简单啦
这里出现了Menu.js里也用到的cloneElement,我上面讲过它的作用了,我们每一层都需要重新克隆下一层的组件,而不是直接,这里可以看到unitKey进行了路径拼接,用上一层的key+当前key,group组件主要是一个菜单的分组,没有功能,只是展示组名。
import React from 'react';
import style from './Menu.less';
function cloneItem(child, index, unitKey) {
return React.cloneElement(child, {index: index, tkey: child.key,unitKey: unitKey + '-' + child.key});
}
function Group(props) {
return (
<div className={style.group}>
<div className={style.group_name} style={{'paddingLeft': props.index*24}}>
<div className={style.border}>
{props.title}
</div>
</div>
<div className={style.group_box}>
{props.children.map(item => {
return (<div key={item.key}>
{cloneItem(item, props.index, props.unitKey)}
</div>)
})}
</div>
</div>
)
}
export default Group;
4. 接下来是SubItem组件,这是个比较关键的组件了,它控制了可展开项,比较复杂
先讲讲hasKey这个函数,还记得之前的unitKey吗?可以理解它是一个路径,而这个函数就是判断当前unitKey是否在这个选中全路径内并且是第一位。如果是的话title显示高亮,也就是加了个类名‘sub_selected’,cloneItem还是和之前一样,已经介绍过了,最后checkItem这个方法是点击展开事件,没错上面讲的是高亮即选中,现在是展开,defaultOpenKeys这个全局共享的展开项,通过修改它来实现切换,index用来判读当前项是否展开
import React from 'react';
import { useState, useContext, useEffect } from 'react';
import style from './Menu.less';
import {TestContext} from './Context';
function hasKey(unitKey, key) {
key = key || "";
return key.indexOf(unitKey) !== 0;
}
function cloneItem(child, index, lastUnitKey) {
return React.cloneElement(child, {index: index + 1, tkey: child.key, unitKey: lastUnitKey + "-" + child.key});
}
function SubItem(props) {
const {defaultSelectedKeys, defaultOpenKeys, setDefaultOpenKeys, uniqueOpened} = useContext(TestContext);
var , = defaultOpenKeys.indexOf(props.tkey);
function checkItem() {
if (index !== -1) { // 存在 排除该项
setDefaultOpenKeys(defaultOpenKeys.filter((res, tindex) => {
return tindex !== index;
}))
} else {
if (uniqueOpened) { // 保持最多只展开一项
setDefaultOpenKeys([props.tkey])
} else { // 随便展开多少项
setDefaultOpenKeys([...defaultOpenKeys, props.tkey])
}
}
}
return (
<div className={`${style.sub_box} ${index !== -1 ? style.sub_active:null} ${hasKey(props.unitKey, defaultSelectedKeys) ? style.sub_selected:null}`}>
<div style={{'paddingLeft': props.index*24}} className={style.title} onClick={() => {
checkItem();
}}>
<span className={style.text}>{props.title}</span>
<span className={`${style.icon} iconfont icon-fenye-shangyiye1`}></span>
</div>
<div className={style.sub_list} style={{'display': index !== -1 ? 'block':'none'}}>
{props.children.map(item => {
return (<div className={style.sub_item} key={item.key}>
{cloneItem(item, props.index, props.unitKey)}
</div>)
})}
</div>
</div>
)
}
export default SubItem
5. 到最后一个组件了Item.js,是的他就是菜单选项
setDefaultSelectedKeys这个是共享的修改选中项的函数,通过unitKey对比defaultSelectedKeys默认选中项来确定是否选中,这里我们看到了paddingLeft包括上面subitem group组件都有对它进行计算,我们用克隆技术设置了index,每一层会加1所以我们能确定当前是那一层,于是就能确定paddingLeft值。
import React from 'react';
import {useContext, useRef, useEffect} from 'react';
import style from './Menu.less';
import {TestContext} from './Context';
function Item(props) {
const {defaultSelectedKeys, setDefaultSelectedKeys} = useContext(TestContext);
useEffect(() => {
if (props.tkey === defaultSelectedKeys) {
setDefaultSelectedKeys(props.unitKey);
}
})
function checkItem() {
setDefaultSelectedKeys(props.unitKey);
if (props.onClick) {
props.onClick();
}
}
return (
<div className={`${style.item} ${props.unitKey === defaultSelectedKeys ? style.active : null}`} style={{'paddingLeft': props.index*24}} onClick={checkItem}>{props.children}</div>
)
}
export default Item
总结: 终于码完了,写的不好的地方我这里先道歉,因为写文章水平真的很一般,上面是完整代码除了一个Context.js,这个比较简单就不贴了,很希望大佬们能提一些建议,还处在学习的阶段,最后:鸣谢