本文的主题是 Steps 组件的设计与实现。Steps 组件是 Steps 步骤和 Timeline 组件结合的组件,在此以前他们是两个不一样的组件,在 NutUI 最近一次版本升级的时候将他们合二为一了,来看看在组件的开发过程当中是如何一步步实现组件功能的。javascript
说到 NutUI , 可能有些人还不太了解,容咱们先简单介绍一下。 NutUI 是一套京东风格的移动端Vue组件库,开发和服务于移动 Web 界面的企业级前中后台产品。经过 NutUI ,能够快速搭建出风格统一的页面,提高开发效率。目前已有 50+ 个组件,这些组件被普遍使用于京东的各个移动端业务中。html
在此以前他们要分开使用,可是又有不少功能是交叉的,并且并不能知足步骤和时间同时出现的业务场景,所以将他们进行了合并。vue
先来看下 Steps 组件的最终呈现效果,数据展现,并带有一些流程性的逻辑。java
组件的功能:node
通常来讲在物流信息、流程信息等内容的展现须要使用到这个组件,能够像下面这样使用它。数组
<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>
等等这样简单的使用方式就能够实现组件的功能。ide
这样设计组件是至关优秀的,由于使用者用的时候真的很是方便简单。函数
这样简单而优雅的组件设计方式适用于大多数功能简单的组件,可是对于逻辑相对复杂、布局也比较复杂的组件来讲就不合适了。布局
功能相对复杂的组件,会让组件变得很不灵活,模板固定,使用自由度很低,对于开发者来,组件编码也会变得十分臃肿。性能
因此在 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
,并根据父组件的属性信息更新子组件。
跑起来验证下,彷佛实现想要的效果!!!
可是,在实际项目应用中,发如今动态刷新这块存在很大问题。
例如:
在刚开始甚至用了比较笨拙的方法,将渲染子组件用到的 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 实例有两个配置选项:
这两个属性是 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
中的属性的。
解决了跨层级嵌套的问题,还有另外一个问题,监听父组件属性的变化。由于:
provide
和inject
绑定并非可响应的。
好比 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.$watch
和 watch
方法中监听是相同的效果,能够主动触发监听,this.$watch()
回返回一个取消观察函数,用来中止触发回调。 这里在组件挂载完成后监听 index
的变化,index
变化再当即触发 current
属性变化的监听。
这样就能实时得到父组件的属性变化了,实现数据监听刷新组件。
至此这个组件的主要难点就攻克了。
固然这种方式只适用于父子层级比较深的场景,同层级兄弟组件之间是没法经过这种方式实现通讯的。
另外 provide
和 inject
主要适用于开发高阶组件或组件库的时候使用,在普通的应用程序代码中最好不要使用。由于这可能会形成数据混乱,业务于逻辑混杂,项目变得难以维护。
在组件开发过程当中,为了保证组件的灵活性、总体性,不少组件都会出现这种嵌套问题,甚至深层嵌套致使的属性共享问题、数据监听问题,那么本文主要根据 Steps 组件的开发经验提供一种解决方案,但愿对你们有那么一丢丢的帮助或启发。