从Antd 源码到自我实现之 Menu 导航菜单

4 篇文章 0 订阅

Antd Menu 简述

Menu 为页面和功能提供导航的菜单列表。

导航菜单是一个网站的灵魂,用户依赖导航在各个页面中进行跳转。一般分为顶部导航和侧边导航,顶部导航提供全局性的类目和功能,侧边导航提供多级结构来收纳和排列网站架构。

要点提取

在这里插入图片描述

核心实现

Menu 的用法。

  <Menu selectedKeys={["analysis"]} >
      <Menu.Item index="analysis">分析</Menu.Item>
      <Menu.Item index="market">营销</Menu.Item>
      <Menu.SubMenu index="set" title={<span>设置</span>}>
          <Menu.Item index="4">二级标题</Menu.Item>
          <Menu.Item index="5">二级标题</Menu.Item>
          <Menu.SubMenu index="sub3" title={<span>二级菜单</span>}>
              <Menu.Item  index="6">三级标题</Menu.Item>
          </Menu.SubMenu>
      </Menu.SubMenu>
  </Menu>

通过上面的 Demo, 我们可以看出 Menu 组件的三个核心部分:Menu, Menu.Item, Menu.SubMenu

Menu 为一个用例中最顶层组件,SubMenu 的父组件为 SubMenu 或 Menu。MenuItem 作为叶子 (leaf) 节点,父组件为 SubMenu 或 Menu.

在这里插入图片描述
:自实现组件库中 mode 命名有点偏差,vertical 其实对应 antd 中的 inline

Menu.tsx

export default class Menu extends React.Component<IMenuProps, any> {
	static propTypes = {...}
	static defaultProps = {...}
	
    static Item = MenuItem;
    static SubMenu = SubMenu;

    static childContextTypes = {
        level: PropTypes.number,
        mode: PropTypes.string,
        onClick: PropTypes.func,
        onSelect: PropTypes.func,
        selectedKeys: PropTypes.arrayOf(PropTypes.string)
    };
    
    getChildContext() {
        return {
            level: 1,
            mode: this.props.mode,
            onClick: this.onClick,
            onSelect: this.onSelect,
            selectedKeys: this.state.selectedKeys
        };
    }

	constructor(props) {
        super(props);
        this.state = {
            selectedKeys: this.props.selectedKeys || []
        };
    }

	onSelect = (params: ClickParam) => {
        this.setState({
            selectedKeys: [params.key]
        });

        if (this.props.onSelect) {
            this.props.onSelect(params);
        }
    }

    onClick = (params: ClickParam) => {
        const {onClick} = this.props;
        if (typeof onClick === 'function') {
            onClick(params);
        }
    }

	  render() {
        const {
            classPrefix,
            className,
            style,
            children,
            mode,
            onClick,
            selectedKeys,
            ...restProps
        } = this.props;

        return (
            <div
                className={classNames(className, `${classPrefix}-container`, {

                })}
                ref="menus-container"
                style={style}
                {...restProps}
            >
                <ul
                    className={classNames(classPrefix, {
                        [`${classPrefix}-vertical`]: mode === 'vertical',
                        [`${classPrefix}-horizontal`]: mode === 'horizontal'
                    })}
                >
                    {children}
                </ul>
            </div>
        );
    }
}

上述对 Menu 实现的要点如下:
1. 负责菜单布局:比如水平 (horizontal),垂直 (vertical),内嵌 (inline)
2. 菜单项 (MenuItem, SubMenu) 选中或点击的事件代理: 将 Menu 的 onClick 与 onSelect 事件回调作为 context 传递到子组件,在 MenuItem 或 SubMenu 的 click 事件中调用,达到一个类似于事件代理的效果
3. 菜单顶层 level = 1: Menu 作为最顶层组件,将 level 作为 context 传递到子组件,作为一个标的。SubMenu 自己的 context 中也有 level,值为父级 Level + 1。由于 MenuItem 是叶子节点,它的 level 即为父级 context.level + 1.
在React中,如果父组件和祖先组件具有相同的 context 字段名,父组件会覆盖祖先组件。因此通过level 的层层自增,我们总能取得当前节点的 level 值。

SubMenu.tsx:

import {Transition} from 'react-transition-group';
import Popover from '../Popover/Popover';
...

export default class SubMenu extends React.Component<ISubMenuProps, any> {
	static propTypes = {
		...
		disabled: PropTypes.bool,
        key: PropTypes.string,
        expanded: PropTypes.bool
	}
	static defaultProps = {...}
	
	// 来自父组件 Menu 或 SubMenu
	static contextTypes = {
        level: PropTypes.number
    };
    // 作为父组件传递 context
    static childContextTypes = {
        level: PropTypes.number
    };
    // 可能有 subMenu -> subMenu 情况
    getChildContext() {
        return {
            level: this.context.level + 1
        };
    }
	
	...
	render() {
		const {...} = this.props;
		const {mode, level} = this.context;
		// Menu 为横向布局时
		const popMenuItems = (
            <ul className={`${classPrefix}-pop`} style={{minWidth: 100}}>
                {children && <div className={`${classPrefix}-pop-content`}>{children}</div>}
            </ul>
        );
        // SubMenu 本体
        const subMenu = (
            <div
                className={`${classPrefix}-title`}
                onClick={this.onTitleClick.bind(this)}
            >
                <div className={`${classPrefix}-title-content`} style={style}>
                    {title}
                    {mode === 'vertical' && <Icon type={`arrow-${this.state.expanded ? 'up' : 'down'}`} className={`${classPrefix}-arrow`}/>}
                </div>
            </div>
        );

return (
            <li
                {...restProps}
                key={key}
                ref="submenu"
                className={classNames(classPrefix, className, {
                    [`${classPrefix}-disabled`]: disabled,
                    [`${classPrefix}-horizontal`]: mode === 'horizontal' && level === 1
                })}
            >
                {
                    mode === 'horizontal'
                    ? <Popover
                        direction={level === 1 ? 'bottom' : 'rightTop'}
                        content={popMenuItems}
                        className={...}
                        trigger="hover"
                    >
                        {subMenu}
                    </Popover>
                    : subMenu
                }
                <Transition ... in={this.state.expanded}>
                    <ul className={`${classPrefix}-sub`}>
                        {children}
                    </ul>
                </Transition>
            </li>
        );
	}
}

上述对 SubMenu 的实现要点如下:

  • 根据 mode 的不同有不同的实现
    • mode 为 horizontal 时使用 Popover 组件,menuItem 以 “弹出” 的形式展现(主要考虑到的交互是 SubMenu hover 时展现 MenuItem)

    • mode 为 vertical 时正常展现,不用 Popover. 展开时的动画效果由第三方控件 Transition 提供。

MenuItem.tsx:

export default class MenuItem extends React.Component<IMenuItemProps, any> {
	static propTypes = {
		...
		index: PropTypes.string // item 的唯一标志
	}
	static defaultProps = {...}
	
    static contextTypes = {
        level: PropTypes.number,
        mode: PropTypes.string,
        onClick: PropTypes.func,
        onSelect: PropTypes.func,
        selectedKeys: PropTypes.arrayOf(PropTypes.string)
    };
constructor(props) {
        super(props);
        this.state = {
            expanded: false,
            isSelected: false
        };
    }

    componentWillReceiveProps(nextProps, nextContext) {
        const selectedKeys = nextContext.selectedKeys;
        const key = this.props.index;
        if (selectedKeys.indexOf(key) > -1) {
            this.setState({
                isSelected: true
            });
        } else {
            this.setState({
                isSelected: false
            });
        }
    }

    componentDidMount() {
        const selectedKeys = this.context.selectedKeys;
        const key = this.props.index;
        if (selectedKeys.indexOf(key) > -1) {
            this.setState({
                isSelected: true
            });
        } else {
            this.setState({
                isSelected: false
            });
        }
    }

    onClickItem(e) {
        this.setState({isSelected: !this.state.isSelected}, () => {
            const {index} = this.props;
            const params: ClickParam = {
                domEvent: e,
                item: this,
                key: index,
                keyPath: [index]
            };
            this.context.onSelect(params);
            this.context.onClick(params);
        });
    }

    render() {
        const {
            classPrefix,
            className,
            index,
            style,
            disabled,
            children,
            ...restProps
        } = this.props;

        const {isSelected, expanded} = this.state;
        const {mode, level} = this.context;

        const menuItem = (
            <div
                className={classNames(`${classPrefix}-content`, {
                    [`${classPrefix}-content-selected`]: isSelected,
                    [`${classPrefix}-vertical-content`]: mode === 'vertical',
                    [`${classPrefix}-horizontal-content`]: mode === 'horizontal'
                })}
                style={style}
            >
                {children}
            </div>
        );

        return (
            // TODO: Tooltip is for inlineCollapsed, set visible to false tempararily
            <li
                {...restProps}
                className={classNames(classPrefix, className, {
                    [`${classPrefix}-vertical`]: mode === 'vertical',
                    [`${classPrefix}-horizontal`]: mode === 'horizontal',
                    [`${classPrefix}-vertical-selected`]: isSelected && mode === 'vertical',
                    [`${classPrefix}-horizontal-selected`]: isSelected && mode === 'horizontal' && level === 1,
                    [`${classPrefix}-disabled`]: disabled
                })}
                onClick={!disabled && this.onClickItem.bind(this)}
            >
                <Tooltip
                    visible={expanded}
                    direction="right"
                    message={menuItem}
                    className={classNames(`${classPrefix}-tooltip`, {

                    })}
                    trigger="hover"
                    showArrow={false}
                >
                    {menuItem}
                </Tooltip>
            </li>
        );
    }
}

上述对 MenuItem 的实现要点如下:

  • 实现关键在于从 Menu 或 SubMenu 组件传递的 context

    • 点击 MenuItem 时同时触发 SubMenu 或 Menu 通过 context 传递来的 click 回调

    • mode 与 level 会影响 MenuItem 的展现,这也是为什么 mode 与 level 需要通过 context 传递过来。

更多组件

更多组件自实现系列,更多文章请参考:

从Antd 源码到自我实现之 Form表单

从Antd 源码到自我实现之 Grid 栅格系统

React 实现 Modal 思路简述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
本系统的研发具有重大的意义,在安全性方面,用户使用浏览器访问网站时,采用注册和密码等相关的保护措施,提高系统的可靠性,维护用户的个人信息和财产的安全。在方便性方面,促进了校园失物招领网站的信息化建设,极大的方便了相关的工作人员对校园失物招领网站信息进行管理。 本系统主要通过使用Java语言编码设计系统功能,MySQL数据库管理数据,AJAX技术设计简洁的、友好的网址页面,然后在IDEA开发平台中,编写相关的Java代码文件,接着通过连接语言完成与数据库的搭建工作,再通过平台提供的Tomcat插件完成信息的交互,最后在浏览器中打开系统网址便可使用本系统。本系统的使用角色可以被分为用户和管理员,用户具有注册、查看信息、留言信息等功能,管理员具有修改用户信息,发布寻物启事等功能。 管理员可以选择任一浏览器打开网址,输入信息无误后,以管理员的身份行使相关的管理权限。管理员可以通过选择失物招领管理,管理相关的失物招领信息记录,比如进行查看失物招领信息标题,修改失物招领信息来源等操作。管理员可以通过选择公告管理,管理相关的公告信息记录,比如进行查看公告详情,删除错误的公告信息,发布公告等操作。管理员可以通过选择公告类型管理,管理相关的公告类型信息,比如查看所有公告类型,删除无用公告类型,修改公告类型,添加公告类型等操作。寻物启事管理页面,此页面提供给管理员的功能有:新增寻物启事,修改寻物启事,删除寻物启事。物品类型管理页面,此页面提供给管理员的功能有:新增物品类型,修改物品类型,删除物品类型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值