NutUI 组件源码揭秘

前言

本文的主题是 Steps 组件的设计与实现。Steps 组件是 Steps 步骤和 Timeline 组件结合的组件,在此之前他们是两个不同的组件,在 NutUI 最近一次版本升级的时候将他们合二为一了,来看看在组件的开发过程中是如何一步步实现组件功能的。

说到 NutUI, 可能有些人还不太了解,容我们先简单介绍一下。 NutUI是一套京东风格的移动端Vue组件库,开发和服务于移动 Web 界面的企业级前中后台产品。通过 NutUI,可以快速搭建出风格统一的页面,提升开发效率。目前已有 50+ 个组件,这些组件被广泛使用于京东的各个移动端业务中。

在此之前他们要分开使用,但是又有很多功能是交叉的,而且并不能满足步骤和时间同时出现的业务场景,因此将他们进行了合并。

先来看下 Steps 组件的最终呈现效果,数据展示,并带有一些流程性的逻辑。

组件的功能:

  1. 根据不同场景采用不同的布局方式
  2. 可以指定当前所在的节点
  3. 可以横向或者纵向排列
  4. 能够动态响应数据的变化

一般来说在物流信息、流程信息等内容的展示需要使用到这个组件,可以像下面这样使用它。

<nut-steps type="mini">
<nut-step title="已签收" content="您的订单已由本人签收。如有疑问您可以联系配送员,感谢您在京东购物。" time="2020-03-03 11:09:96" />
<nut-step title="运输中" content="您的订单已达到京东【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step content="您的订单已达到京东【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step content="您的订单由京东【北京顺义分拣中心】送往【北京旧宫营业部】" time="2020-03-03 11:09:06" />
<nut-step title="已下单" content="您提交了订单,请等待系统确认" time="2020-03-03 11:09:06"/>
</nut-steps>

组件封装的思路

大多数的组件是一个单独的组件,使用起来很简单,比如我们 NutUI 组件库中的 <nut-button block>默认状态</nut-button><nut-icon type="top"></nut-icon> 等等这样简单的使用方式就可以实现组件的功能。

这样设计组件是相当优秀的,因为使用者用的时候真的非常方便简单。

这样简单而优雅的组件设计方式适用于大多数功能简单的组件,但是对于逻辑相对复杂、布局也比较复杂的组件来说就不合适了。

功能相对复杂的组件,会让组件变得很不灵活,模板固定,使用自由度很低,对于开发者来,组件编码也会变得十分臃肿。

所以在 vue 组件开发过程中合理使用插槽 slot 特性,让组件更加的灵活和开放。就像下面这样:

<nut-tab @tab-switch="tabSwitch">
<nut-tab-panel tab-title="页签一">这里是页签1内容</nut-tab-panel>
<nut-tab-panel tab-title="页签二">这里是页签2内容</nut-tab-panel>
<nut-tab-panel tab-title="页签三">这里是页签3内容</nut-tab-panel>
<nut-tab-panel tab-title="页签四">这里是页签4内容</nut-tab-panel>
</nut-tab> <nut-subsidenavbar title="人体识别1" ikey="9">
<nut-sidenavbaritem ikey="10" title="人体检测1"></nut-sidenavbaritem>
<nut-sidenavbaritem ikey="11" title="细粒度人像分割1"></nut-sidenavbaritem>
</nut-subsidenavbar> ...

有很多相对复杂的组件采用这种方式,既能保证组件功能的完整性,也能自由配置子元素内容。

组件的实现

基于上面的设计思路,就可以着手实现组件了。

本文的 Steps 组件,包含外层的 <nut-steps> 和内层的 <nut-step> 两个部分。

我们一般会这样设计

<-- nut-steps -->
<template>
<div class="nut-steps" :class="{ horizontal: direction === 'horizontal' }">
<slot></slot>
</div>
</template>
<-- nut-step -->
<template>
<div class="nut-step clearfix" :class="`${currentStatus ? currentStatus : ''}`">
...
</div>
</template>

外层组件控制整体组件的布局,激活状态等,子组件主要渲染内容,但是他们之间的关联成了难题。

子组件中的一些状态逻辑需要由父组件来控制,这就存在父子组件之间属性或状态的通信。

解决这个问题有两种思路,一是在父组件中获取子组件信息,再将子组件需要的父组件信息给子组件设置上,二是在子组件中获取父组件的属性信息来渲染子组件。

第一种方案:

this.steps = this.$slots.default.filter((vnode) => !!vnode.componentInstance).map((node) => node.componentInstance);
this.updateChildProps(true);

首先通过 this.$slots.default 获取到所有的子组件,然后在 updateChildProps 中遍历 this.steps ,并根据父组件的属性信息更新子组件。

跑起来验证下,似乎实现想要的效果!!!

Prop 动态更新

但是,在实际项目应用中,发现在动态刷新这块存在很大问题。

例如:

  1. 当前所处状态发生改变需要遍历所用子组件,性能低下
  2. 子组件内容或某个属性变化,想要更新组件会变得异常麻烦
  3. 父组件中要维护管理很多子组件的属性

在刚开始甚至用了比较笨拙的方法,将渲染子组件用到的 list 传递给父组件,并监听该属性的变化情况来重新渲染子组件。但是为了实现这种更新却添加了一个毫无意义的数据监听,还需要深度监听,而部分场景下也并不是必须,重新遍历渲染子组件也会造成性能消耗,效率低下。

所以这种方式并不合适,改用第二种方式。

在子组件中访问父组件的属性,利用 this.$parent 来访问父组件的属性。

// step 组件创建之前将组件实例添加到父组件的 steps 数组中
beforeCreate() {
this.$parent.steps.push(this);
}, data() {
return {
index: -1,
};
}, methods: {
getCurrentStatus() {
// 访问父组件的逻辑更新属性
const { current, type, steps, timeForward } = this.$parent;
// 逻辑处理
}
},
mounted() {
// 监听 index 的变化重新计算相关逻辑
const unwatch = this.$watch('index', val => {
this.$watch('$parent.current', this.getCurrentStatus, { immediate: true });
unwatch();
});
}

在父组件中,接收子组件实例并设置 index 属性

data() {
return {
steps: [],
};
},
watch: {
steps(steps) {
steps.forEach((child, index) => {
child.index = index; // 设置子组件的 index 属性,将会用于子组件的展示逻辑
});
}
},

通过下面这张图来看下它的数据变化。

子组件中的属性变化只依赖子组件的属性,子组件内部的属性变化并不需要触发父组件的更新,而子组件数量的变化会触达父组件,并按照创建顺序给子组件重新排序设定 index 值,子组件再根据 index 值的变化重新渲染。

将更多的逻辑交给了子组件处理,而父组件更多的是做整体组件的功能逻辑。也不必要监听子组件的数据源也能更新组件。

但是,实现过程中有个关键属性可能是造成 bug 的重要隐患,它就是 this.$parent .

只有子组件 <step> 的父级是 <steps> 时访问到的 this.$parent 才是准确的。

如果不是直接的父子级就一定会出现 bug 。

实际使用中,不仅是这个组件,其他这类组件也会出现子组件的直接父级并不是它对应父级的情况,这就会产生 bug 。比如:

<nut-steps :current="active">
<nut-row>
<nut-step v-for="(step, index) in steps" :key="index" :title="step.title" :content="step.content" :time="step.time">
</nut-step>
</nut-row>
</nut-steps>

<nut-row> 组件作为 <nut-step> 组件的父级组件的时候, this.$parent 指向的就不是 <nut-steps> 了。

那么在 <nut-step> 中可以加一些 hack:

let parent = this.$parent || this.$parent.$parent;

但这很快就会失控,治标不治本,再加几层嵌套,立刻玩完。

多层传递的神器 - 依赖注入

现在主要要解决的问题是让后代子组件访问到父级组件实例上的属性或方法,中间不管跨几级。

vue 依赖注入可以派上用场了。

vue 实例有两个配置选项:

  1. provide: 指定我们想要提供给后代组件的数据/方法。
  2. inject:接收指定的我们想要添加在这个实例上的 property 。

这两个属性是 vue v2.2.0 版本新增

这两选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果熟悉 React,这与 React 的上下文特性很相似。

父组件使用 provide 提供可注入子孙组件的 property 。

// 父级组件 steps
provide() {
return {
timeForward: this.timeForward,
type: this.type,
pushStep: this.pushStep,
delStep: this.delStep,
current: this.current,
}
}, methods: {
pushStep(step) {
this.steps.push(step);
},
delStep(step) {
const steps = this.steps;
const index = steps.indexOf(step);
if (index >= 0) {
steps.splice(index, 1);
}
}
},

子组件使用 inject 读取父级组件提供的 property 。

// 子孙组件 step
inject: ['timeForward', 'type', 'current', 'pushStep', 'delStep']
// beforeCreate() {
// this.$parent.steps.push(this);
// // this.pushStep(this);
// },
created() {
this.pushStep(this);
},

子组件不再使用 this.$parent 来获取父级组件的数据了。

这里有个细节,子组件更新父组件的 steps 值的时机从 beforeCreate 变成了 created ,这是因为 inject 的初始化是在 beforeCreate 之后执行的,因此在此之前是访问不到 inject 中的属性的。

解决了跨层级嵌套的问题,还有另一个问题,监听父组件属性的变化。因为:

provideinject 绑定并不是可响应的。

比如 current 属性是可以动态改变的,像上面这个注入,子孙组件拿到的永远是初始化注入的值,并不是最新的。

这个也很容易解决,在父组件注入依赖时使用函数来获取实时的 current 值即可。

  provide() {
return {
getCurrentIndex: () => this.current,
}
},

在子组件中:

  computed: {
current() {
return this.getCurrentIndex();
}
}, mounted() {
const unwatch = this.$watch('index', val => {
this.$watch('current', this.getCurrentStatus, { immediate: true });
unwatch();
});
},

this.$watchwatch 方法中监听是相同的效果,可以主动触发监听,this.$watch() 回返回一个取消观察函数,用来停止触发回调。 这里在组件挂载完成后监听 index 的变化,index 变化再立即触发 current 属性变化的监听。

这样就能实时获得父组件的属性变化了,实现数据监听刷新组件。

至此这个组件的主要难点就攻克了。

当然这种方式只适用于父子层级比较深的场景,同层级兄弟组件之间是无法通过这种方式实现通信的。

另外 provideinject 主要适用于开发高阶组件或组件库的时候使用,在普通的应用程序代码中最好不要使用。因为这可能会造成数据混乱,业务于逻辑混杂,项目变得难以维护。

总结

在组件开发过程中,为了保证组件的灵活性、整体性,很多组件都会出现这种嵌套问题,甚至深层嵌套导致的属性共享问题、数据监听问题,那么本文主要根据 Steps 组件的开发经验提供一种解决方案,希望对大家有那么一丢丢的帮助或启发。

Steps 组件的设计与实现的更多相关文章

  1. ASP.NET通用权限组件思路设计

    开篇 做任何系统都离不开和绕不过权限的控制,尤其是B/S系统工作原理的特殊性使得权限控制起来更为繁琐,所以就在想是否可以利用IIS的工作原理,在IIS处理客户端请求的某个入口或出口通过判断URL来达到 ...

  2. Unity3d&C#分布式游戏服务器ET框架介绍-组件式设计

    前几天写了<开源分享 Unity3d客户端与C#分布式服务端游戏框架>,受到很多人关注,QQ群几天就加了80多个人.开源这个框架的主要目的也是分享自己设计ET的一些想法,所以我准备写一系列 ...

  3. 前端开发组件化设计vue,react,angular原则漫谈

    前端开发组件化设计vue,react,angular原则漫谈 https://www.toutiao.com/a6346443500179505410/?tt_from=weixin&utm_ ...

  4. atitti.atiNav 手机导航组件的设计

    atitti.atiNav 手机导航组件的设计 1.1. 三大按键导航功能,back,menu ,home1 1.2. header页头组件,为移动页面顶部的导航条设计.1 1.3. 页头主题设计1 ...

  5. Python-S9-Day88——stark组件之设计urls

    03 stark组件之设计urls 04 stark组件之设计urls2 05 stark组件之设计list_display 06 stark组件之z查看页面的数据展示 03 stark组件之设计ur ...

  6. 通用异步 Windows Socket TCP 客户端组件的设计与实现

    编写 Windows Socket TCP 客户端其实并不困难,Windows 提供了6种 I/O 通信模型供大家选择.但本座看过很多客户端程序都把 Socket 通信和业务逻辑混在一起,剪不断理还乱 ...

  7. 基于 IOCP 的通用异步 Windows Socket TCP 高性能服务端组件的设计与实现

    设计概述 服务端通信组件的设计是一项非常严谨的工作,其中性能.伸缩性和稳定性是必须考虑的硬性质量指标,若要把组件设计为通用组件提供给多种已知或未知的上层应用使用,则设计的难度更会大大增加,通用性.可用 ...

  8. 微服务架构案例(05):SpringCloud 基础组件应用设计

    本文源码:GitHub·点这里 || GitEE·点这里 更新进度(共6节): 01:项目技术选型简介,架构图解说明 02:业务架构设计,系统分层管理 03:数据库选型,业务数据设计规划 04:中间件 ...

  9. 自定义vant ui steps组件效果实现

    记录个问题,当作笔记吧:因为vue项目的移动端vant ui 的step组件跟ui设计图有差别,研究了半天还是没法使用step组件,只能手动设置一个 先上效果图和代码: (1)HTML部分 <d ...

随机推荐

  1. Learn day7 继承(单、多、菱形)\多态\魔术方法\装饰器\异常

    1.继承 1.1 单继承# ### 继承 """ (1) 单继承 (2) 多继承 至少2个类,子类和父类 一个类继承另外一个类,当前类是子类 (衍生类) 被继承的这个类是 ...

  2. ERP的财务凭证的操作与设计--开源软件诞生23

    赤龙ERP财务凭证讲解--第23篇 用日志记录"开源软件"的诞生 [进入地址 点亮星星]----祈盼着一个鼓励 博主开源地址: 码云:https://gitee.com/redra ...

  3. 『JVM』我不想知道我是怎么来滴,我就想知道我是怎么没滴

    我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在 ...

  4. Photoshop CC 习惯设置

    安装后,一般设置: 1.编辑--首选项--常规 2.一般更改内容为: 性能 内存使用情况在50%-80%之间 暂存盘:除去C盘意外的其他盘 单位和标尺:以px为单位 其他根据喜好设定!

  5. 分库分表的 9种分布式主键ID 生成方案,挺全乎的

    <sharding-jdbc 分库分表的 4种分片策略> 中我们介绍了 sharding-jdbc 4种分片策略的使用场景,可以满足基础的分片功能开发,这篇我们来看看分库分表后,应该如何为 ...

  6. 带货直播源码开发采用MySQL有什么优越性

    MySQL是世界上最流行的开源关系数据库,带货直播源码使用MySQL,可实现分钟级别的数据库部署和弹性扩展,不仅经济实惠,而且稳定可靠,易于运维.云数据库 MySQL 提供备份恢复.监控.容灾.快速扩 ...

  7. 获取url后面的参数

    function getQueryVariable(variable) { var query = window.location.search.substring(1); var vars = qu ...

  8. SPI的学习和ESP8266的SPI通讯测试

    SPI简介: SPI是串行外设接口(Serial Peripheral Interface)的缩写.SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时 ...

  9. ESP8266交叉编译环境变量设置

    在build目录下执行sudo cp -r xtensa-lx106-elf /opt 修改xtensa-lx106-elf目录权限:这一步非常重要,否则在后续编译中很容易出现没有权限问题:sudo ...

  10. 2. Hive常见操作命令整理

    该笔记主要整理了<Hive编程指南>中一些常见的操作命令,大致如下(持续补充中): 1. 查看/设置/修改变量2. 执行命令3. 搜索相关内容4. 查看库表信息5. 创建表6. 分区7. ...