likes
comments
collection
share

多榜单注入差异化组件方案

作者站长头像
站长
· 阅读数 30

背景

榜单落地页是一个专门用来展示榜单列表的页面,榜单类型有热销榜、好评榜、回购榜等,这些榜单的请求链路大体是一致的,它们的数据逻辑可复用,但是在页面UI上是有差异的,不同榜单页面的整体UI结构是一样的,在页面局部上会有不同的展示,比如热销榜要展示销量,不用展示评分,而好评榜要展示评分,不用展示销量。

故而,我们需要提供一套方案实现前端界面的整体UI统一,局部差异解耦。

方案探索

v-ifv-else控制

v-ifv-else条件语句可以实现控制组件内部不同的UI展示,但是随着榜单类型的增加,这些代码耦合在一起,充斥着if (RankType === 'A')的代码,会越来越难以维护。实际业务逻辑上可能只有热销榜才会展示销量标签组件,用条件判断实现的话,每种榜单都会去判断要不要展示销量标签组件。因此不推荐用v-ifv-else控制。

维护map去做组件映射也是类似的道理,存在多次判断、需要维护多个map的问题:

const compAMap = {
  RankA: compA,
  RankB: null,
  ...
}

const compBMap = {
  RankA: null,
  RankB: compB,
  ...
}

在多层级组件中使用vue插槽

项目使用的是vue框架,所以一开始是想用插槽实现,思路为在榜单布局组件中将不同的UI组件用插槽预留好位置进行占位,每个榜单提供自己的容器组件,在容器组件中通过vue的插槽机制填充注入这些UI组件。理想很美好,但是在实践过程中,发现存在差异的UI组件和入口的容器组件中间可能存在n层组件传递,vue在多层级组件中实现插槽的传递太麻烦了。

插槽的示例如下:

Root.vue

<script>
import Parent from './Parent.vue'
  
export default {
  components: {
    Parent
  },
  data () {
    return {
      a: 1
    }
  }
}
</script>

<template>
<Parent>
    <template v-slot:demo="{ b }">
      {{ a + b }}
    </template>
</Parent>
</template>

Parent.vue

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  }
}
</script>

<template>
<Child>
    <template v-slot:demo="{ b }">
      <slot name="demo" :b="b"></slot>
    </template>
</Child>
</template>

Child.vue

<script>

export default {
  data () {
    return {
      b: 2
    }
  }
}

</script>

<template>
  <div>
    <slot name="demo" :b="b" />
  </div>
</template>

从上面的示例代码就可以很清晰地看到,在多层级组件中使用vue插槽,需要在每一层的组件中编写类似下面的代码进行透传:

<template v-slot:demo="{ b }">
    <slot name="demo" :b="b"></slot>
</template>

每一层组件都要写,太繁琐了,而且这么写一眼看上去不知道在执行什么,影响代码的可读性,万一中间有一层组件写漏了,需要跟踪排查是哪一层组件出问题,维护成本也会增加,因此最终未采用该方案。

provide/inject差异化的组件

技术假想

在Vue.js中,provide和inject是一对用于实现依赖注入的功能。provide和inject允许你在一个组件的祖先组件中提供数据,并在任何后代组件中注入和使用这些数据,不论它们之间的层级关系有多深。

相比插槽机制,provide和inject的优势是不用在每一层组件去声明,使用起来简单很多。

那么我们是不是可以把每种榜单存在差异的组件看作是一份数据,在榜单的容器组件中通过provide/inject注入给后代组件使用呢?类似于下面这样的代码:

// RootA.vue
provide('RANK_COMP_TOKEN', {
  CompA,
  CompB: null,
})

// RootB.vue
provide('RANK_COMP_TOKEN', {
  CompA: null,
  CompB,
})

// 后代组件
const { CompA } = inject('RANK_COMP_TOKEN')
<Component v-if="CompA" :is="CompA" />

源码阅读/可行性分析

用provide和inject去注入组件,会不会有什么问题呢?好像没见过有人这么使用过。我们来看一下provide/inject的源码实现。

我们先看Vue.prototype._init这个方法,它是Vue内部用来初始化组件实例的核心方法,在https://github.com/vuejs/vue/blob/main/src/core/instance/init.ts可以找到:

vm._self = vm
initLifecycle(vm) // 划重点
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // 划重点,resolve injections before data/props
initState(vm)
initProvide(vm) // 划重点,resolve provide after data/props
callHook(vm, 'created')

可以看到vue是通过initProvide和initInjections两个方法来初始化配置provide/inject,源码主要在:https://github.com/vuejs/vue/blob/main/src/core/instance/inject.ts

initProvide方法里面会去调用resolveProvided这个方法,我们要关注resolveProvided这个方法的实现

export function resolveProvided(vm) {
  const existing = vm._provided
  const parentProvides = vm.$parent && vm.$parent._provided
  if (parentProvides === existing) {
    return (vm._provided = Object.create(parentProvides))
  } else {
    return existing
  }
}

vm._provided是在initLifecycle方法里面初始化好的:vm._provided = parent ? parent._provided : Object.create(null), 也就是说在resolveProvided方法里,后代组件对于parentProvides === existing是为真, 然后对vm._provided进行重新设置:vm._provided = Object.create(parentProvides)

Object.create用于创建一个新对象,并通过指定的原型对象(即另一个对象或null)来设置新对象的原型,就是说,在vm._provided = Object.create(parentProvides)执行后,vm._provided可以通过原型链访问到parentProvides。

在initProvide方法中,我们在业务代码中写的vm.$options.provide会被复制到vm._provide上。在initInjections方法中会在vm._provide查找,然后赋值到vm上。

原来如此,provide/inject的核心原理就是javascript的原型链!

通过provide/inject去注入组件,就是把这些差异化的组件挂在vm._provide的原型链上。

落地实践

我们最终确定,通过provide/inject去注入差异化组件的方案,代码设计如下:

- rank
  - Container.vue // 榜单入口容器组件
  - common // 榜单公共代码
    - Item.vue
    - List.vue
  - rank-a // a榜单
    - Container.vue // a榜单容器组件
    - Label.vue // 差异化组件
  - rank-b // b榜单
    - Container.vue // b榜单容器组件
    - Label.vue // 差异化组件

榜单入口容器组件会根据榜单类型去导入使用a榜单或者b榜单容器组件,a/b榜单容器组件使用了统一的布局组件List.vue和Item.vue,a/b榜单容器组件分别注入各自的差异化组件。

收益:从目录结构和容器组件代码中就可以很清晰地看出组件的归属关系,通过RANK_COMP_TOKEN这种标识可以快速找出依赖的相关代码,后面每新增一种榜单,都可以参照已有榜单的实现,只处理差异化的组件即可。

转载自:https://juejin.cn/post/7367253337417826343
评论
请登录