Vue2混入和Vue3中hook函数的应用

一、Vue2 的混入(Mixin)

在开发过程中,我们经常会发现一些可重复利用的代码块,于是我们将其封装成函数以供调用。这类函数包括工具函数,但又不止工具函数,因为我们可能也会封装一些重复的业务逻辑。但在以往,在前端原生开发中,我们所封装的函数,大多数是”无状态”的,不能够建立数据与视图之间的联系。那何为”有状态”的函数呢?这里的”有状态”,实际上是指是否含有响应式变量:它的变化能够引起视图的变化。这也是混入和普通函数的最大区别!

混入 (Mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

需要注意的是:

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

1
2
3
4
var mixin = { data: function () { return { message: 'hello', foo: 'abc' } } }
new Vue({ mixins: [mixin], data: function () { return { message: 'goodbye', bar:
'def' } }, created: function () { console.log(this.$data) // => { message:
"goodbye", foo: "abc", bar: "def" } } })

同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

1
2
3
var mixin = { created: function () { console.log('混入对象的钩子被调用') } } new
Vue({ mixins: [mixin], created: function () { console.log('组件钩子被调用') } })
// => "混入对象的钩子被调用" // => "组件钩子被调用"

值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

1
2
3
4
5
var mixin = { methods: { foo: function () { console.log('foo') }, conflicting:
function () { console.log('from mixin') } } } var vm = new Vue({ mixins:
[mixin], methods: { bar: function () { console.log('bar') }, conflicting:
function () { console.log('from self') } } }) vm.foo() // => "foo" vm.bar() //
=> "bar" vm.conflicting() // => "from self"

二、使用混入实现功能权限的管控

在页面上,我们经常要检查用户是否拥有某个功能点的权限。这种带状态的功能,很适合使用混入实现

通常一个功能点,对应一个权限标识符

image-20241209144118987

一般我们已将用户信息(包括用户所拥有的权限信息)放在了状态管理工具中。我们要做的就是看看用户,是否拥有point-user-delete这个 point,有就可以让删除能用,没有就隐藏或者禁用

src/mixin/checkPermission.js

1
2
3
4
5
6
7
8
9
10
11
12
import store from '@/store'
export default {
methods: {
checkPermission(key) {
const { userInfo } = store.state.user
if (userInfo.roles.points && userInfo.roles.points.length) {
return userInfo.roles.points.some((item) => item === key)
}
return false
},
},
}

注册之后在员工组件中检查权限点

1
2
3
4
5
6
<el-button
:disabled="!checkPermission('POINT-USER-UPDATE')"
type="text"
size="small"
@click="$router.push(`/employees/detail/${obj.row.id}`)"
>查看</el-button>

三、Mixin 的不足

在 Vue 2 中,Mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
1、Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。也就是说 Mixin 难以追溯的方法与属性,我们根本无法获知属性来自于哪个 Mixin 文件,给后期维护带来困难
2、可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。

四、Vue3 的 Hooks

得益于 Vue3 的组合式 API,在 Vue3 中使用 Hooks 可以弥补混入的不足。也正因为如此,Vue3 中推荐使用 Hooks 代替 Mixin

五、动态计算表格高度

我们经常使用到的一种场景是,头部是一些查询条件,中间是查询结果表格,底部是分页组件(或者没有)。因为查询条件数量是不确定的,因此这一块的高度也是不确定的。此时,表格的高度(或者最大高度)需要动态计算

src/hooks/useTable.js

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
import { onMounted, ref, watch, onActivated, onDeactivated } from 'vue'
import { useWindowSize } from '@vueuse/core'

export default function useTable(tableRef, bottom) {
// // console.log('tableRef',tableRef)
// // console.log(tableRef.$el);
const bottomOptions = {
noPagination: 32,
withPagination: 32 + 52,
}

let lock = false
const tableHeight = ref()
const { height } = useWindowSize()

onMounted(() => {
calcTableHeight()
})

onActivated(() => {
// // console.log("onActivated");
lock = false
calcTableHeight()
})

onDeactivated(() => {
// // console.log("onDeactivated");
lock = true
})

watch(height, () => {
calcTableHeight()
})

function calcTableHeight() {
if (lock) return
// // console.log("hshshsh");
const { top } = tableRef.value.$el.getBoundingClientRect()
let currentBottom = 0
if (typeof bottom === 'string') {
currentBottom = bottomOptions[bottom] || 0
} else {
currentBottom = bottom
}
tableHeight.value = height.value - top - currentBottom
// // console.log(tableHeight)
}
return {
tableHeight,
calcTableHeight,
}
}

在组件中使用

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
<template>
<el-table v-loading="loading" :data="tableList" :height="tableHeight">
<el-table-column type="index" width="55" align="center" label="序号" />
<el-table-column
prop="deviceTypeName"
min-width="150"
label="设备类型"
align="center"
/>
<el-table-column
prop="deviceName"
min-width="150"
label="设备名称"
align="center"
/>
<el-table-column
prop="depict"
min-width="240"
label="设备描述"
align="center"
/>
<el-table-column
prop="deviceNum"
min-width="90"
label="设备数量"
align="center"
/>
</el-table>
</template>
import useTable from '@/hooks/useTable' const { tableHeight } =
useTable(tableRef, 'withPagination')

除此之外,我们还可以将查询字典功能(例如,一些状态、类型、原因等等的名称和 id 的对照)做成 Hooks 函数。这里只提供一下思路,不做具体实现了。在 Hooks 函数中,接收两个参数,第一个是字典的类型,第二个是字典的值(名称和 id 均可)。在字典数据中(这个可以通过 http 每次去请求,也可以一次请求回来所有数据,并将其放在状态管理工具中),根据传入的参数,筛选出对应的那条字典值,并将其交出去

六、实现付款倒计时逻辑

src/hooks/useCountDown.js

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
// 封装倒计时逻辑函数
import { computed, onUnmounted, ref } from 'vue'
import dayjs from 'dayjs'
export const useCountDown = () => {
// 1. 响应式的数据
let timer = null
const time = ref(0)
// 格式化时间 为 xx分xx秒
const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
// 2. 开启倒计时的函数
const start = (currentTime) => {
// 开始倒计时的逻辑
// 核心逻辑的编写:每隔1s就减一
time.value = currentTime
timer = setInterval(() => {
time.value--
}, 1000)
}
// 组件销毁时清除定时器
onUnmounted(() => {
timer && clearInterval(timer)
})
return {
formatTime,
start,
}
}

在页面中使用

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
<script setup>
import { getOrderAPI } from '@/apis/pay'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useCountDown } from '@/composables/useCountDown'
const { formatTime, start } = useCountDown()
// 获取订单数据
const route = useRoute()
const payInfo = ref({})
const getPayInfo = async () => {
const res = await getOrderAPI(route.query.id)
payInfo.value = res.result
// 初始化倒计时秒数
start(res.result.countdown)
}
onMounted(() => getPayInfo())

// 跳转支付
// 携带订单id以及回调地址跳转到支付地址(get)
// 支付地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const backURL = 'http://127.0.0.1:5173/paycallback'
const redirectUrl = encodeURIComponent(backURL)
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`
</script>

<template>
<div class="xtx-pay-page">
<div class="container">
<!-- 付款信息 -->
<div class="pay-info">
<span class="icon iconfont icon-queren2"></span>
<div class="tip">
<p>订单提交成功!请尽快完成支付。</p>
<p>
支付还剩 <span>{{ formatTime }}</span
>, 超时后将取消订单
</p>
</div>
<div class="amount">
<span>应付总额:</span>
<span>¥{{ payInfo.payMoney?.toFixed(2) }}</span>
</div>
</div>
<!-- 付款方式 -->
<div class="pay-type">
<p class="head">选择以下支付方式付款</p>
<div class="item">
<p>支付平台</p>
<a class="btn wx" href="javascript:;"></a>
<a class="btn alipay" :href="payUrl"></a>
</div>
<div class="item">
<p>支付方式</p>
<a class="btn" href="javascript:;">招商银行</a>
<a class="btn" href="javascript:;">工商银行</a>
<a class="btn" href="javascript:;">建设银行</a>
<a class="btn" href="javascript:;">农业银行</a>
<a class="btn" href="javascript:;">交通银行</a>
</div>
</div>
</div>
</div>
</template>
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 congtianfeng
  • 访问人数: | 浏览次数: