项目开发中遇到的一个小需求,公共组件 GlobalHeader 上增加一个合规中心入口,点击后跳转合规相关页面,同时要拿到合规相关信息回填到页面上。这里先不考虑实际业务场景,就单把公共组件的实现流程,做个复盘吧,小伙伴们发现问题还请批评指出
目的:
公共组件 pro-core 的 GlobalHeader 上增加一个合规中心入口:
- 页面加载调用合规相关接口,并回填数据
- 暴露一个刷新方法,业务模块能调用实现后续逻辑
- 该入口能点击,enableStatus == 1 配置过跳合规分数页,未配置跳 合规介绍页(这里有个优化判断,当前页面和跳转 url 相当就 return 掉)
- 临时新增:要拿到授权信息 accessList 判断是否有权限来展示(拿已有的 v-auth 指令来改了)
初步分析:
- 业务模块往往嵌套很深,且不是在同一个项目,一般方式拿不到 Vue 节点和实例
- 公共组件也会多多级嵌套,毕竟是组件,尽量别依赖 pinia 等
- 既然是写公共组件,考虑到后续扩展性,尽可能定义好类型,写好文档
实现思路:
- 公共组件中定义好相关方法,通过 defineExpose 暴露实例或对应方法
- 业务组件获取到对应的 Vue 组件实例
- 通过拿到实例的方式,调用其暴露的方法或其他属性
简单归纳一下,就是麻烦版的组件通信
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', ] 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) { if (inst.parent) { return getTargetInstExposed(inst.parent, targetName) } else { return null } } else { result = inst if (result.type.name === targetName) { console.log(`找到了符合的实例 -->`, result) return result.exposed } } }
|
- 首先,检查 inst 节点的类型是否与 targetName 相同。
- 如果类型不匹配,则检查 inst 是否有父节点。 a. 如果存在父节点,则继续递归查找。 b. 如果不存在父节点,则返回 null。
- 如果类型匹配,则将找到的实例赋值给 result,并检查 result 的 exposed 属性是否符合条件。 a. 如果 exposed 属性符合条件,则返回 exposed 属性。 b. 如果 exposed 属性不符合条件,则继续递归查找。
实现效果:
祭出 Demo 大法
具体问题定位与分析
这里记录一下:
- accessList 在新账号,或未开通权限的情况下接口返数据为 null ,这里必须加上 const accessList = appStore.saasUserStore.accessList || [] 默认值,不然会报错
- useSaaSEnv() 这个公共包的依赖可能需要其他 lib 的版本,合规 OK,但较早的业务链路项目报错,目前去掉了,都使用从容器 config 接口获取合规 url 的方式
- 原本在组件 mounted 时调用了接口,但这里既然做了权限判断,应改为,只有通过 v-auth 指令后,(在指令 onMounted 阶段,has 为 true )再执行接口调用
优化点导致的问题:
接口调用后将数据存到了 storage,本意是省一次请求。但由于 Header 与 业务项目渲染时机不同,
组件在渲染时:(组件比 公共组件的 header 渲染更快)不一定能拿到正确的 storage 数据,导致后续逻辑判断错误
目前在业务中,是在 beforeEnter 组件路由钩子上去请求的,这会导致,初始没数据时会多请求一次,虽然解决了问题… 但后续想到好的优化点再进行优化吧