1

Menu开启数据传递新姿势

 2 years ago
source link: https://jelly.jd.com/article/62eb190205de4d019ef15359
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JELLY | Menu开启数据传递新姿势

Menu开启数据传递新姿势
上传日期:2022.08.04
本篇文章主要讲述 NutUI 中的 Menu 组件内部是怎么实现父子组件传递数据的,Menu 组件通常以下拉菜单的形式出现,选项都在弹出层上,不占用页面空间,常见于移动端的条件筛选,比如商品的筛选、排序的筛选等等。

众所周知,Vue 父子组件数据双向传递的方式有很多,网上也有很多相关文章。这里大概罗列几个:

  • props & event 父组件通过 props 传递数据给子元素,子元素通过触发事件向父组件通信。这种方式使用简单,也是最容易理解的,适合嵌套不深的祖孙组件之间传递数据,相反如果组件嵌套层数很深的话,这种方式就显得比较笨重。

  • Provide & Inject 通常用于嵌套比较深的祖孙组件之间数据传递,避免了层层通过 props 传递。

  • Vuex/Pinia 这种方式适合存储公共的数据,在业务项目开发中比较常用,组件库中不建议使用。

肯定还有别的方式,这里不再一一赘述了。

本篇文章要介绍的 Menu 组件来自 NutUI 组件库,NutUI 是一个京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用。那么在 NutUI 中 Menu 组件内部是怎么实现父子组件传递数据的呢?现在就让我带领大家认识一下我们的 Menu 组件,然后一层一层去揭开他的面纱。

Menu 组件通常以下拉菜单的形式出现,选项都在弹出层上,不占用页面空间,常见于移动端的条件筛选,比如商品的筛选、排序的筛选等等。

11d36b920f7b79f2.png

市面上也有一些组件库实现了 Menu,但是总体给人的感觉是功能偏少,下面是我整理的所有包含 Menu 组件的组件库中关于此组件的功能对比:

功能NutUIVantTDesign MobileMand Mobile
自定义菜单内容
自定义选中态颜色
向上展开
禁用菜单
一行两列
滚动置顶
自定义图标
自定义标题样式类
树型下拉菜单

很明显可以看出,NutUI 中的 Menu 组件功能是非常完善的,不仅如此,使用方式也很简单:

<nut-menu>
    <nut-menu-item v-model="state.value" :options="options" />
</nut-menu>

介绍完了 Menu 组件的功能和如何使用,下面重点来看下他的内部实现。在正式介绍他的内部实现前,必须先了解下他的整体 Dom 结构。

DOM 结构

Menu 组件的完成需要两个组件的配合,即 Menu 组件本身以及他的子组件 MenuItem,两个组件分工明确,当然内部也不乏引用几个基础的组件,包括 Popup 和 Icon。

11d36b920f7b79f2.png

从图中隐隐约约可以看到数据传递的影子,比如标题中 children 和 Item 中的 parent,没错,这就是本文要讲的数据传递新方式,那么究竟是什么新方式呢?这里大家可以先留个印象,然后带着这个疑问继续往下看实现思路。

属性实现引路

先让我们看几个内外部属性的实现思路吧,我将他们分为几类。

1、父组件通过 children 对象获取数据

  • disabled 禁用: 每个 Menu 组件通常会包含很多 MenuItem 组件,为了保证不影响其他 MenuItem 组件的正常使用,disabled 属性设置在了 MenuItem 组件上,父组件通过 children 对象拿到设置的具体值,并传递给自己内部的标题 item 元素,给其添加上 disabled 的样式类,从而实现了某些 MenuItem 组件选择性禁用。
11d36b920f7b79f2.png
<template v-for="(item, index) in children" :key="index">
  <view
    class="nut-menu__item"
    :class="{ disabled: item.disabled}"
  >
    <view class="nut-menu__title">
      <view class="nut-menu__title-text">{{ item.renderTitle() }}</view>
      <nut-icon
        :name="item.titleIcon"
      ></nut-icon>
    </view>
  </view>
</template>
  • titleIcon 标题图标: 同样为了保证可以为每个子组件的标题单独设置图标,titleIcon 的属性必须设置在 MenuItem 上,父组件通过 children 拿到并渲染到标题里,代码见楼上。
11d36b920f7b79f2.png
  • renderTitle 渲染标题: 这里不是要说 title 属性,而是当不设置 title 属性时,代码内部渲染标题的方式 renderTitle。renderTitle 方法实现很简单,首先判断是否有 title 属性,如果有直接返回,否则通过正则匹配选项的 value 值,匹配成功就返回对应选项的 text 属性值,截图见楼上。
const renderTitle = () => {
  if (props.title) {
    return props.title;
  }

  const match: any = props.options?.find((option: any) => option.value === props.modelValue);

  return match ? match.text : '';
};

2、 子组件通过 parent 对象获取 props

  • overlay & close-on-click-overlay & ock-scroll & duration: 这些属性都被设置在了 Menu 组件上,名字大家应该都很眼熟,没错,他们是要被透传给 MenuItem 组件的 Popup 组件使用的,设置在 Menu 组件上,也是方便同时控制所有的 MenuItem 组件。这里可以看到父子传递数据没有使用传统的 props 的方式,而是通过 parent 这个对象中获取的。
<nut-popup
  :duration="parent.props.duration"
  :overlay="parent.props.overlay"
  :lockScroll="parent.props.lockScroll"
  :close-on-click-overlay="parent.props.closeOnClickOverlay"
>
  ...
</nut-popup>
  • active-color 自定义颜色: 此属性用来设置选中标题和选中菜单项的用户样式,父子组件都要用到,故放到父组件上比较恰当。
11d36b920f7b79f2.png
<template v-for="(item, index) in children" :key="index">
  <view
    class="nut-menu__item"
    :style="{ color: item.state.showPopup ? activeColor : '' }"
  >
    <view class="nut-menu__title">
      ...
    </view>
  </view>
</template>
<view class="nut-menu-item__content">
  <view
    v-for="(option, index) in options"
    :key="index"
    class="nut-menu-item__option"
  >
    <nut-icon v-if="option.value === modelValue" :name="optionIcon" :color="parent.props.activeColor"></nut-icon>
    <view :style="{ color: option.value === modelValue ? parent.props.activeColor : '' }">{{ option.text }}</view>
  </view>
  <slot></slot>
</view>
  • direction 向下/上展开: 此属性也是父子组件内部实现都要使用的属性,父组件通过它判断标题使用哪个方向的 Icon,以及获取不同情况下 Menu 距离浏览器的距离,提供给 MenuItem 组件的 Popup 组件使用。子组件通过它为 Popup 设置不同的样式和方向。
11d36b920f7b79f2.png
<view class="nut-menu__title">
  <view class="nut-menu__title-text">{{ item.renderTitle() }}</view>
  <nut-icon
    :name="item.titleIcon || (direction === 'up' ? 'arrow-up' : 'down-arrow')"
    size="10"
    class="nut-menu__title-icon"
  ></nut-icon>
</view>
<nut-popup
  :style="
    parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
  "
  :overlayStyle="
    parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
  "
>
  ...
</nut-popup>

3、子组件通过 parent 对象获取 offset(内部数据传递)

Parent 对象除了为 MenuItem 组件提供 props,还提供了 offset,主要用于根据菜单展开方向为 Popup 组件设置不同的内联样式。

<nut-popup
  :style="
    parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
  "
  :overlayStyle="
    parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
  "
>
  ...
</nut-popup>

最后用一张图再总结下,父子组件传递的所有数据:

11d36b920f7b79f2.png

重点方法揭秘

说完了以上属性的实现思路,你一定好奇 parent 和 children 这两个是怎么实现的,现在让我们来揭开他们神秘的面纱。

1、 父组件内定义 children,通过执行 linkChildren 方法把 props 和 offset 传递给子组件,这样子组件就可以通过 "parent." 的形式使用 Menu 组件的属性。

11d36b920f7b79f2.png

核心代码如下:

const useChildren = () => {
  const publicChildren: any[] = reactive([]);

  const linkChildren = (value?: any) => {
    const link = (child: any) => {
      if (child.proxy) {
        publicChildren.push(child.proxy as any);
      }
    };

    provide(
      'menuParent',
      Object.assign(
        {
          link,
          children: publicChildren
        },
        value
      )
    );
  };

  return {
    children: publicChildren,
    linkChildren
  };
};

const { children, linkChildren } = useChildren();

linkChildren({ props, offset });

2、 子组件内定义 parent,通过 Vue 的内置方法 getCurrentInstance 获取子组件的实例,并传递给 parent 的 link 方法,link 方法内部就会通过 child.proxy 拿到子组件的所有属性,这样在父组件内部就会随意使用子组件的属性了。

先来看下从 Menu 组件里成功获取到的 MenuItem 组件的部分属性截图:

11d36b920f7b79f2.png

核心代码如下:

const useParent: any = () => {
  const parent = inject('menuParent', null);

  if (parent) {
    // 获取子组件自己的实例
    const instance = getCurrentInstance()!;

    const { link } = parent;

    link(instance);

    return {
      parent
    };
  }
};

const { parent } = useParent();

从以上分析可以看到实现思路也并不复杂,当然内部还是基于 provide & inject 的。

新模式优缺点

  • 优点:父组件可以很轻松获取到子组件的所有 props,不仅如此,还能获取到子组件 setup 方法中返回的所有数据,比如前面介绍的 renderTitle 方法。
  • 缺点:代码理解起来可能比较绕,但是如果把思路理清,很容易搞懂的。

截止到这里,代码已全部讲解完成,不知道你理解了没。

这种方式很适合开发包含子组件的组件,希望通过对 Menu 组件的介绍给大家带来这类组件开发的新思路。另外文章代码片段是针对每个实现细节的,完整代码请参考官方 git 仓库 。如果您有别的实现思路,也请留言讨论,让咱们共同探索 Menu 组件的最佳实现方式。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK