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. Miniconda 安装 & Pip module 安装 & Shell 脚本调用 Miniconda 虚拟环境手册(实战项目应用)

    (实战项目应用) 1. 下载Miniconda 两个安装方式: 方式1:wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Min ...

  2. 考场(NOIP/ICPC)沙雕错误锦集(大赛前必看,救命提分良药)

    记住,无论什么测试,一定要先打三题暴力(至少不会被屠得太惨) 2018.10.4 1.记得算内存.(OI一年一场空,没算内存见祖宗) 2018.10.6 1.在二分许多个字符串时(二分长度),要以长度 ...

  3. Java_进程与线程

    进Process&Thread 区别 进程 线程 根本区别 作为资源分配的单位 调度和执行的单位 开销 每个进程都有独立的代码和数据空间(进程上下文), 进程间的切换会有较大的开销 线程可以看 ...

  4. 为研发同学定制的MySQL面试指南 - “能谈谈基数统计吗?”

    ** 目录 推荐阅读原文链接 一.基数是啥? 二.InnoDB更新基数的时机? 三.基数是估算出来 四.持久化基数 四.如何主动更新基数? 欢迎关注 Hi,大家好!我是白日梦. 今天我要跟你分享的话题 ...

  5. C# type对象

    新建控制台应用程序 新建一个类 class MyClass { private int id; private int age; public int numb; public string Name ...

  6. layui表单提交

    关于layui表单提交  只是简单用一个文本框记录一下提交过程    其他的如下拉框选择框样式可以参考官网 下面直接开始.首 一:前台页面 <!DOCTYPE html><html& ...

  7. MCscan-Python-jcvi 共线性画图最后一章更新

    经过几轮调试和修改,共线性图终于可以上眼了.如下: 图中红色的为目标基因,蓝色的为reference species目标基因周围15个基因,天蓝色为再往外15个基因,黄色为与reference spe ...

  8. 基于FFmpeg的Dxva2硬解码及Direct3D显示(三)

    初始化Direct3D 目录 初始化Direct3D 创建Direct3D物理设备对象实例 创建Direct3D渲染设备实例 创建Direct3D视频解码服务 Direct3D渲染可以通过Surfac ...

  9. menuconfig

    1. menuconfig 的存在意义 原由是 项目的 config 项太多了,需要一个人性化的方式设置. menuconfig 背后是一个应用程序,用户和该应用程序交互,完成 config 设置. ...

  10. 讲武德,你们要的高性能日志工具 Log4j2,来了

    Log4j 介绍过了,SLF4J 介绍过了,Logback 也介绍过了,你以为日志系列的文章就到此终结了? 不不不,我告诉你,还有一个 Log4j 2,顾名思义,它就是 Log4j 的升级版,就好像手 ...