React学习日志(Menu组件篇)

453 阅读4分钟

前言: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,这个比较简单就不贴了,很希望大佬们能提一些建议,还处在学习的阶段,最后:鸣谢