草庐IT

折叠面板组件的设计与实现

京东JDC于明明 2023-03-28 原文

​前言

NutUI,大家应该不陌生吧,前端开发的同学肯定是有些了解的。NutUI 是一个京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用。

目前 NutUI 拥有 70+ 组件,支持按需引用,支持 TypeScript,支持定制主题等功能,当然也支持最新的 Vue3 语法,在开发上能有效帮助研发人员提升效率,改善开发体验。

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

折叠面板设计

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

在组件开发阶段,我们通常都会进行对比分析,取长补短。所以我们简单通过功能上的对比来入组件的开发。

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

让我们一起来探索吧~

实现展开收起

组件的基本交互已经明了,那我们的标题和内容的布局方式就比较简单了。现在我们需要去完成交互的开发,也就是展开折叠的功能。

实现展开折叠的功能其实很简单,就是通过一个变量控制内容的展示隐藏就可以了,不用考虑其他因素的情况下,这种方法的确是最高效的方式。

<template>
<div>
<div @click="handle">
标题
</div>
<div v-show="show">
测试内容测试内容测试内容测试内容测试内容测试内容
</div>
</div>
</template>
<script setup>
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 实现收起展开的动画效果。

灵活的标题栏

其次就是标题栏功能的完善,增加图标及自定义位置和相关动画功能。我们先来看下基本用法的右侧图标,它和内容的收起展开是相呼应的,交互上展开时是上箭头收起时是下箭头。那么我们根据是否展开的状态为变量,使用一个箭头图标就可以轻松搞定。实现的方案就是利用 css3 的 rotate 属性,反转 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">
<div>
<slot name="extraRender"></slot>
</div>
</view>
在这里既然提到了 slot,我就多?嗦一下[憨笑]。关于上述提到的标题及内容的展示,设计的时候考虑能让开发者省时省力,有更多的可操作性,基本上都是以 slot 的形式来接收入参(仅限于本组件,内容展示相关),这样的话即使后端或者前端处理数据携带 HTMl 标签也可以轻松识别,无需多余处理。

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

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

开发设计番外

01Scss 中使用变量

这个功能大家想必也不陌生,说白了就是可以通过 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 的概念。

02组件开发适配

想成为 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);
});
});
通过以上方法可以获取到节点的信息,包括 width、height、x、y 等,大家可以体验试一下查看获取的信息。还有一点需要注意,就是在给元素设置 style 样式时,最好是在组件中使用 style 变量接收,不要直接赋值。

// 类似这种方式改变 style
const style = reactive({
color: 'red',
height: '100px',
});


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

03vue3 组件通信

在组件开发时,因为 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 的组件都可以作为其所有下级组件的依赖提供者。

provide 和 inject 的实现原理主要是利用了原型和原型链来实现。

在 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 先获取当前组件的实例对象,然后判断是否根组件,如果是根组件则返回到 appContext 的 provides,否则就返回父组件的 provides。如果当前的 key 在 provides 上有值,就返回该值,反之则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过 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,非常欢迎大家参与共建。

有关折叠面板组件的设计与实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐