关于javascript:折叠面板组件的设计与实现

37次阅读

共计 6776 个字符,预计需要花费 17 分钟才能阅读完成。

前言

NutUI,大家应该不生疏吧 [鬼脸],前端开发的同学必定是有些理解的。NutUI 是一个京东格调的挪动端组件库,应用 Vue 语言来编写能够在 H5,小程序平台上的利用。

目前 NutUI 领有 70+ 组件,反对按需援用,反对 TypeScript,反对定制主题等性能,当然也反对最新的 Vue3 语法,在开发上能无效帮忙研发人员晋升效率,改善开发体验。

言归正传,明天咱们一起理解 NutUI 中折叠面板 Collapse 的实现与设计,以及在开发过程中学习到的新知识点。

折叠面板设计

其实折叠面板组件无论是在 PC 还是 M,都是比拟常见的组件,顾名思义就是能够折叠 / 开展的内容区域。应用场景也比拟宽泛,例如导航、文字类详情、筛选分类等;

在组件开发阶段,咱们通常都会进行比照剖析,舍短取长。所以咱们简略通过性能上的比照来入组件的开发。

折叠面板 vant antd tdesign elementui varlet vuetify naiveui iview balam nutui
内容开展收起
动画成果
手风琴模式
折叠图标 icon
折叠图标 color
折叠图标 size
题目图标地位
旋转角度
副标题
反对禁用模式
可设置固定内容

组件的实质就是晋升开发效率的,咱们通过对业务场景的解构和组合配置形式实现业务需要。好比组件库是一个工具箱,每个组件就是箱子里的扳手、钳子等工具,为业务场景提供各种工具,如何去打造一个适合趁手的工具干活,就须要咱们对平时的业务开发有所理解和思考。

让咱们一起来摸索吧~

实现开展收起

组件的根本交互曾经明了,那咱们的题目和内容的布局形式就比较简单了。当初咱们须要去实现交互的开发,也就是开展折叠的性能。

 

 

实现开展折叠的性能其实很简略,就是通过一个变量管制内容的展现暗藏就能够了,不必思考其余因素的状况下,这种办法确实是最高效的形式。

<template>
  <div class="container">
    <div class="title" @click="handle">
      题目
    </div>
    <div class="content" v-show="show">
      测试内容测试内容测试内容测试内容测试内容测试内容
    </div>
  </div>
</template>
<script setup lang="ts">
    import {ref} from 'vue';
    const show = ref(false);
    const handle = () => {show.value = !show.value;}
</script>

然而采纳这种形式可能对咱们前期的性能扩大和交互成果不太敌对。所以我是计划是通过扭转折叠内容的 height 的形式实现的,当然实现这个办法也比拟好了解。

咱们次要解决 content 的内容,对于这块款式咱们对它的 height 默认是 0,也就是内容是折起的状态。因为每个折叠内容是无奈确定的,所以咱们须要动静计算内容填充后的高度,这种形式也算是一种适配计划。

我动静计算的目标是为了实现前面动画成果,晋升用户体验感。我利用的是 height + transform 的形式实现的,同时应用 css 的属性 will-change 对动画成果进行优化。

will-change 为 web 开发者提供了一种告知浏览器该元素会有哪些变动的办法,这样浏览器能够在元素属性真正发生变化之前提前做好对应的优化筹备工作。这种优化能够将一部分简单的计算工作提前准备好,使页面的反馈更为疾速灵活。

// 组件局部外围代码
const wrapperRefEle: any = wrapperRef.value;
const contentRefEle: any = contentRef.value;
if (!wrapperRefEle || !contentRefEle) {return;}
const offsetHeight = contentRefEle.offsetHeight || 'auto';
if (offsetHeight) {const contentHeight = `${offsetHeight}px`;
    wrapperRefEle.style.willChange = 'height';
    wrapperRefEle.style.height = !proxyData.openExpanded ? 0 : contentHeight;
}

以上代码就是通过获取元素的 DOM 来计算出内容的高度 offsetHeight 并赋值,通过高度的变动联合 transform 实现收起开展的动画成果。

 

 

灵便的标题栏

其次就是标题栏性能的欠缺,减少图标及自定义地位和相干动画性能。咱们先来看下根本用法的右侧图标,它和内容的收起开展是相响应的,交互上开展时是上箭头收起时是下箭头。那么咱们依据是否开展的状态为变量,应用一个箭头图标就能够轻松搞定。实现的计划就是利用 css3rotate 属性,反转 180° 就能够了。

if (parent.props.icon && !proxyData.openExpanded) {proxyData.iconStyle['transform'] = 'rotate(0deg)';
} else {proxyData.iconStyle['transform'] = 'rotate(' + parent.props.rotate + 'deg)';
}

为了用户的自定义性更高,更好的扩大组件能力,对外裸露了对于图标配置的 API,比方自定义图标、图标的旋转角度等。这些配置参考不同场景,比方某些新闻报道的内容折叠旋转 90°。

 

 

当然,标题栏文字也能够配置相干图标,包含图标的地位、色彩、大小等。这种性能减少了用户的个性化配置,他能够用来展现某些重要音讯、新音讯揭示,未查看信息等场景应用。

 

 

某些组件库的开发者可能没有此配置,首先个人感觉和组件是无关的。组件的设计是须要与业务之间进行连接,形象出一些性能,这样能更好的欠缺组件的性能,包含前期组件的扩大等,都是在业务倒退中成长的。

配置项降级

在前期的应用过程中,咱们依据某些场景对组件性能进行了优化降级。

首先减少了副标题的配置,通过 sub-title 就能够轻松设置(PS: 上图👆可看到示例)。

商城类挪动端中的搜寻分类性能,比方下图的这种场景。它会有默认的内容展现在里面,在折叠后其余内容进行折叠或开展,所以新增了 slot:extraRender API,让这部分内容以插槽的模式存在,不便开发者定义不同的展现模式,便于款式的调整等。

 

 

以上性能的实现也比较简单,就是在代码的中减少一个 slot 标签接管传入的内容即可。

<view v-if="$slots.extraRender" class="collapse-extraWrapper">
    <div class="collapse-extraRender">
        <slot name="extraRender"></slot>
    </div>
</view>

在这里既然提到了 slot,我就多啰嗦一下[憨笑]。对于上述提到的题目及内容的展现,设计的时候思考能让开发者省时省力,有更多的可操作性,基本上都是以 slot 的模式来接支出参(仅限于本组件,内容展现相干),这样的话即便后端或者前端解决数据携带 HTMl 标签也能够轻松辨认,无需多余解决。

 

 

面板既然都能够开展收起操作,那么反之也有禁止操作的。我提供了一个简略的属性设置 disabled 来确定是否可操作,实现形式就是通过设置 style 款式实现的。

.nut-collapse-item-disabled {
    color: #c8c9cc;
    cursor: not-allowed;
    pointer-events: none;
}

开发设计番外

Scss 中应用变量

这个性能大家想必也不生疏,说白了就是能够通过 JS 管制 CSS 的款式,目前 Vue3 反对咱们应用在 CSS 中应用变量,间接上代码。

<template>
  <span>NutUI</sapn>
</template>

<script>
export default {data () {
    return {color: 'red'}
  }
}
</script>

<style vars="{color}" scoped>
span {color: var(--color);
}
</style>

是不是很简略,其实相似的写法,在之前就有相似的插件反对的。

  • emotion
  • jss
  • styled-components
  • aphrodite
  • radium
  • glamor

这些插件大家感兴趣的能够尝试一下,小编用过 styled-components,还是很容易上手的,在上手前倡议大家理解下 CSS-in-JS 的概念。

组件开发适配

想成为 NutUI 的 contributor 吗?如果也想为 NutUI 奉献本人的组件,上面可是适配小程序的一些要点哟~

 

 

H5 开发时获取 DOM 元素是比拟容易的,通过 document 或者 ref 都能够。然而咱们在适配小程序的时候这种形式是获取不到的,须要依据 Taro 提供的办法去获取。

import Taro, {eventCenter, getCurrentInstance as getCurrentInstanceTaro} from '@tarojs/taro';
eventCenter.once((getCurrentInstanceTaro() as any).router.onReady, () => {const query = Taro.createSelectorQuery();
  query.selectAll('.collapse-content').boundingClientRect();
  query.exec((res) => {console.log(res);
  });
});

通过以上办法能够获取到节点的信息,包含 widthheightxy 等,大家能够体验试一下查看获取的信息。还有一点须要留神,就是在给元素设置 style 款式时,最好是在组件中应用 style 变量接管,不要间接赋值。

// 相似这种形式扭转 style
const style = reactive({
    color: 'red',
    height: '100px',
});

const change = () => {style.color = 'blue';}

vue3 组件通信

在组件开发时,因为 nut-collapse nut-collapse-item 父子组件须要进行通信,我应用的是 provide/inject 的形式,所以对此通信形式进行了简略的的学习理解。

对于组件通信的形式,props、emit、attrs 等等形式,大家必然已了然于胸,我就不献丑了。当初我简略和大家分享一下 provide/inject 的传参模式,这个 API 在 vue2 的时候曾经存在。

//a.vue 组件
// 创立一个 provide
import {defineComponent, provide} from 'vue';
export default defineComponent({setup () {
    const msg: string = 'Hello NutUI';
    // provide 进来
    provide('msg', msg);
  }
})
//b.vue 组件
// 接收数据
import {defineComponent, inject} from 'vue'
export default defineComponent({setup () {const msg: string = inject('msg') || '';
  }
})

通过以上 2 个示例,操作是不是非常简单,但须要留神一点,provide 不是响应式的,如果你要使其具备响应性,你须要传入也应该是响应式数据。

provide 提供的数据不思考组件层次结构,也就是发动 provide 的组件都能够作为其所有上级组件的依赖提供者。

provideinject 的实现原理次要是利用了原型和原型链来实现。

在 Vue3 中 provide 函数就是给以后组件实例上的 provides 对象属性,增加键值对 key/value。还有一个中央就是如果以后组件和父级组件的 provides 雷同时,在以后组件实例中的 provides 对象和父级,则建设链接,即原型 prototype

function provide(key, value) {if (!currentInstance) {if ((process.env.NODE_ENV !== 'production')) {warn(`provide() can only be used inside setup().`);
        }
    }
    else {
        // 获取以后组件实例的 provides 属性
        let provides = currentInstance.provides;
        // 获取以后父级组件的 provides 属性
        const parentProvides = currentInstance.parent && currentInstance.parent.provides;
        if (parentProvides === provides) {// Object.create() es6 创建对象的一种形式,能够了解为继承一个对象, 增加的属性是在原型下。provides = currentInstance.provides = Object.create(parentProvides);
        }
        provides[key] = value;
    }
}

对于 inject 的实现我就不多赘述了,大家有趣味的能够去依据源码做更深刻的理解。

从上面代码能够大抵理解,inject 先获取以后组件的实例对象,而后判断是否根组件,如果是根组件则返回到 appContextprovides,否则就返回父组件的 provides。如果以后的 keyprovides 上有值,就返回该值,反之则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过 call 办法把组件实例的代理对象绑定到该函数的 this 上,否则就间接返回默认内容。

function inject(key, defaultValue, treatDefaultAsFactory = false) {
    // 如果是被一个函数式组件调用则取 currentRenderingInstance
    const instance = currentInstance || currentRenderingInstance;
    if (instance) {
    // 如果 intance 位于根目录下,则返回到 appContext 的 provides,否则就返回父组件的 provides
        const provides = instance.parent == null
            ? instance.vnode.appContext && instance.vnode.appContext.provides
            : instance.parent.provides;
        if (provides && key in provides) {return provides[key];
        }
        // 如果参数大于 1 个 第二个则是默认值,第三个参数是 true,并且第二个值是函数则执行函数。else if (arguments.length > 1) {return treatDefaultAsFactory && isFunction(defaultValue)
                ? defaultValue.call(instance.proxy) 
                : defaultValue;
        }
    }
}

大抵能够这么了解 provide API 调用的时候,设置父级的 provides 为以后 provides 对象原型对象上的属性,在 inject 获取 provides 对象中的属性值时,优先获取 provides 对象本身的属性,如果本身查找不到,则沿着原型链向上一个对象中去查找。

总结

本文次要介绍了 NutUI 中 折叠面板 组件的设计思路与实现原理,并分享了一些开发中遇到的问题,心愿能在开发中帮到大家。

如果在开发中遇到问题,可随时提 issue,NutUI 团队的同学都会认真对待并解决问题。如您有好的组件,业务类、通用类的都可,都可向 NutUI 组件库提交 PR,十分欢送大家参加共建。

最初,非常感谢始终以来反对 NutUI 的团队及同学,大家的需要与倡议,让咱们的组件库越来越好,咱们也会一直致力,力争更上一层楼!

来点个 Star ❤️ 反对咱们一下吧 ~

 
文章参考链接:
 

  1. Vue3 的 Provide / Inject 的实现原理:https://juejin.cn/post/7064904368730374180
  2. 一文读懂 vuex4 源码,原来 provide/inject 就是妙用了原型链?:https://juejin.cn/post/6963802316713492516

正文完
 0