项目开发中遇到的一个小需求,公共组件 GlobalHeader 上增加一个合规中心入口,点击后跳转合规相关页面,同时要拿到合规相关信息回填到页面上。这里先不考虑实际业务场景,就单把公共组件的实现流程,做个复盘吧,小伙伴们发现问题还请批评指出

目的:

公共组件 pro-core 的 GlobalHeader 上增加一个合规中心入口:

  1. 页面加载调用合规相关接口,并回填数据
  2. 暴露一个刷新方法,业务模块能调用实现后续逻辑
  3. 该入口能点击,enableStatus == 1 配置过跳合规分数页,未配置跳 合规介绍页(这里有个优化判断,当前页面和跳转 url 相当就 return 掉)
  4. 临时新增:要拿到授权信息 accessList 判断是否有权限来展示(拿已有的 v-auth 指令来改了)

初步分析:

  1. 业务模块往往嵌套很深,且不是在同一个项目,一般方式拿不到 Vue 节点和实例
  2. 公共组件也会多多级嵌套,毕竟是组件,尽量别依赖 pinia 等
  3. 既然是写公共组件,考虑到后续扩展性,尽可能定义好类型,写好文档

实现思路:

  1. 公共组件中定义好相关方法,通过 defineExpose 暴露实例或对应方法
  2. 业务组件获取到对应的 Vue 组件实例
  3. 通过拿到实例的方式,调用其暴露的方法或其他属性

简单归纳一下,就是麻烦版的组件通信

1. 先实现组件功能逻辑,通过 defineExpose 暴露方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<template>
<div
v-auth="authList"
class="compliance-entrance"
@click.stop.prevent="handleClick"
>
<img
width="16"
height="22"
class="compliance-icon"
src="./entry.svg"
/>
<span> {{ complianceInfo.name ?? '合规中心' }} {{ complianceInfo.version ?? '' }} </span>
<div
v-if="complianceInfo.firstEnable == 1"
class="tag-wrap"
>
NEW
</div>
<div
v-if="complianceInfo.firstEnable != 1 && complianceInfo.resultScore"
class="tag-wrap"
>
{{ complianceInfo.resultScore }}
</div>
</div>
</template>

<script
setup
lang="ts"
>
import useComplianceHooks from './useComplianceHooks'
import { useSaaSEnv } from '@medcrab-common/base-data'
const { queryCompliance, complianceInfo } = useComplianceHooks()
const authList = [
'compliance:center',
'compliance:container',
'compliance:settings',
'compliance:view',
'compliance:report:create',
'compliance:report:download',
]
// 公共包里的 v-auth 和业务有些出入这里重实现了一个
const vAuth: Directive = {
mounted: (el: HTMLElement, bind: DirectiveBinding) => {
const appStore = useAppStore()
const accessList = appStore.saasUserStore.accessList
let has = false
if (Array.isArray(bind.value)) {
has = bind.value.some(item => bind.value.includes(item))
} else {
has = accessList.indexOf(bind.value) > -1
}
if (!has) {
return el.parentNode && el.parentNode.removeChild(el)
}
},
}

const handleClick = () => {
useSaaSEnv().withCallback(({ data }) => {
const url = data.COMPLIANCE_URL
const path = `${
complianceInfo.value.enableStatus == 1
? '/compliance/config?enableStatus=1'
: '/compliance/introduce'
}`
if (location.href == `${url}${path}`) return
location.href = `${url}${path}`
})
}

defineExpose({
queryCompliance,
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<!-- 其他代码省略 -->
<CrabButton
@click="setting"
type="text"
highlight
>设置</CrabButton
>
<CrabButton
@click="update"
type="primary"
>更新</CrabButton
>
</div>
</template>

<script
setup
lang="ts"
>
const inst = getCurrentInstance()
</script>

2. 先分析下组件层级

App.vue -> <router-view ref="routerviewRef"> -> BasicLayout(pro-core) -> ComplianceCenter -> compliance-score-card.vue

BasicLayout.vue -> RightContent.vue -> ComplianceEntrance.vue

也就是由 当前组件 ComplianceEntrance 的实例一直找到 BasicLayout 即可
这里写了一个方法,通过递归找到父组件,一直找到节点或找到根节点

需要注意,若要用该方法,父组件往上都需要 defineOptions({ name: ‘componentName’ })

3. getTargetInstExposed

函数接收一个 ComponentInternalInstance 类型的参数 inst 和一个字符串类型的参数 targetName,以及一个可选的 any 类型的参数 result。
函数的目的是从 inst 节点开始,递归查找是否有符合条件的实例,并将找到的实例的 exposed 属性返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import type { ComponentInternalInstance } from 'vue'

export function getTargetInstExposed(
inst: ComponentInternalInstance,
targetName: string,
result?: any
) {
if (inst.type.name != targetName) {
// console.log('没找到 -> 继续找', inst.parent?.type)
if (inst.parent) {
return getTargetInstExposed(inst.parent, targetName)
} else {
// console.log('到了根节点依旧没找到')
return null
}
} else {
result = inst
if (result.type.name === targetName) {
console.log(`找到了符合的实例 -->`, result)
return result.exposed
}
}
}
  1. 首先,检查 inst 节点的类型是否与 targetName 相同。
  2. 如果类型不匹配,则检查 inst 是否有父节点。 a. 如果存在父节点,则继续递归查找。 b. 如果不存在父节点,则返回 null。
  3. 如果类型匹配,则将找到的实例赋值给 result,并检查 result 的 exposed 属性是否符合条件。 a. 如果 exposed 属性符合条件,则返回 exposed 属性。 b. 如果 exposed 属性不符合条件,则继续递归查找。

找到暴露的方法

实现效果:

祭出 Demo 大法

具体问题定位与分析

这里记录一下:

  1. accessList 在新账号,或未开通权限的情况下接口返数据为 null ,这里必须加上 const accessList = appStore.saasUserStore.accessList || [] 默认值,不然会报错
  2. useSaaSEnv() 这个公共包的依赖可能需要其他 lib 的版本,合规 OK,但较早的业务链路项目报错,目前去掉了,都使用从容器 config 接口获取合规 url 的方式
  3. 原本在组件 mounted 时调用了接口,但这里既然做了权限判断,应改为,只有通过 v-auth 指令后,(在指令 onMounted 阶段,has 为 true )再执行接口调用

优化点导致的问题:

接口调用后将数据存到了 storage,本意是省一次请求。但由于 Header 与 业务项目渲染时机不同,
组件在渲染时:(组件比 公共组件的 header 渲染更快)不一定能拿到正确的 storage 数据,导致后续逻辑判断错误

目前在业务中,是在 beforeEnter 组件路由钩子上去请求的,这会导致,初始没数据时会多请求一次,虽然解决了问题… 但后续想到好的优化点再进行优化吧