一、前置知识
1. mixin是什么
mixin
也就是混入,不仅仅在 Vue 框架中存在 mixin,确切的说 mixin 是一种混入的思想,他会自动的将混入的东西准确的分配到指定的组件中。
举个例子:现在组件A 中的 watch
中需要处理的逻辑是 hanldleParams
, 在组件B 中的 watch 中同样需要这样的逻辑 hanldleParams,那么我们应该如何将这两块相同的逻辑抽象出来复用呢?
两种方法:
-
抽函数
:将 hanldleParams 以函数的形式抽出来,然后在 watch 中调用 hanldleParams -
mixin
:上一种抽函数方法虽然可以解决一定的复用问题,但是我们还是需要在组件中写 watch,如果每个组件都写 watch,那么 watch 也是重复的东西,因此 mixin 就是将 watch 钩子都可以抽出来的组件。也就是说,mixin 抽出来不仅仅是纯函数逻辑,还可以将 Vue 组件特有的钩子等逻辑也可以抽出来,达到进一步复用,而且 mixin 中的数据和方法都是独立的,组件之间使用后是互不影响的。
上面这两种方法的区别就代表了mixin和utils的区别
2. mixin解决的问题
mixin 解决了两种复用:
-
逻辑函数的复用
-
Vue 组件配置复用
注意:组件配置复用是指,组件中的选项式API(例如:data,computed,watch)或者组件的生命周期钩子(created、mounted、destroyed)
3. mixin的使用场景
关键:在 Vue中,mixin 定义的就是一个对象,对象中放置的 Vue 组件相应的选项式API和对应的生命周期钩子
export const mixins = {
data() {
return {};
},
computed: {},
created() {},
mounted() {},
methods: {},
};
使用如下:
export const mixins = {
data() {
return {
msg: "我是小猪课堂",
};
},
computed: {},
created() {
console.log("我是mixin中的created生命周期函数");
},
mounted() {
console.log("我是mixin中的mounted生命周期函数");
},
methods: {
clickMe() {
console.log("我是mixin中的点击事件");
},
},
};
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<button @click="clickMe">点击我</button>
</div>
</template>
<script>
import { mixins } from "./mixin/index";
export default {
name: "App",
mixins: [mixins],
components: {},
created(){
console.log("组件调用minxi数据",this.msg);
},
mounted(){
console.log("我是组件的mounted生命周期函数")
}
};
</script>
以上这段代码等价于:
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<button @click="clickMe">点击我</button>
</div>
</template>
<script>
import { mixins } from "./mixin/index";
export default {
name: "App",
mixins: [mixins],
data() {
return {
msg: "我是小猪课堂",
};
},
methods: {
clickMe() {
console.log("我是mixin中的点击事件");
},
},
created(){
console.log("我是mixin中的created生命周期函数");
console.log("组件调用minxi数据",this.msg);
},
mounted(){
console.log("我是mixin中的mounted生命周期函数");
console.log("我是组件的mounted生命周期函数")
}
};
</script>
注意:mixin 中和 Vue 组件中相同的钩子的优先级:
-
mixin 中的生命周期函数会和组件的生命周期函数一起合并执行
-
mixin 中的 data 数据在组件中也可以使用
-
mixin 中的方法在组件内部可以直接调用
-
生命周期函数合并后执行顺序:先执行 mixin 中的,后执行组件的
此外,mixin 对于不同组件的导入,相互之间数据是不会影响的
4. mixin的缺点
(1) 相同钩子中注册的函数名相同会发生冲突(Vue 中冲突的解决方案是本组件中优先级高于 mixin)
(2) 定位错误需要花费时间
(3) 滥用会造成维护问题
二、hooks是什么
一般来说,我们开发中会自动抽象出逻辑函数放在 utils
中,utils 中放的纯逻辑,不存在属于组件的东西;而 hooks
就是在 utils 的基础上再包一层组件级别的东西(钩子函数等)。hooks 和 utils 的区别: hooks 中如果涉及到 ref,reactive,computed
这些 api 的数据,那这些数据是具有响应式的,而 utils 只是单纯提取公共方法就不具备响应式,因此可以把 hooks 理解为加入 Vue3 api
的公共方法。
那么 hooks
相当于组件级别的逻辑封装,这种逻辑封装在 Vue2
中的 mixin 也可以实现,那为什么还要使用 hooks 呢?开篇的时候我们已经了解了 mixin
的缺点,而 hooks 最大的优势是灵活,下面通过两个例子来体验下两者的区别:
1. 使用mixin的例子:
export default {
data() {
return {
name: 'zhng'
}
},
methods: {
setName(name) {
this.name = name
}
}
}
<template>
<div>{{ name }}</div>
<template>
<script>
import nameMixin from './name-mixin';
export default {
mixins: [nameMixin],
mounted() {
setTimeout(() => {
this.setName('Tom')
}, 3000)
}
}
<script>
2. 使用hooks的例子:
import { computed, ref, Ref } from "vue"
type CountResultProps = {
count:Ref<number>;
multiple:Ref<number>;
increase:(delta?:number)=>void;
decrease:(delta?:number)=> void;
}
export default function useCount(initValue = 1):CountResultProps{
const count = ref(initValue)
const increase = (delta?:number):void =>{
if(typeof delta !== 'undefined'){
count.value += delta
}else{
count.value += 1
}
}
const multiple = computed(()=>count.value * 2)
const decrease = (delta?:number):void=>{
if(typeof delta !== "undefined"){
count.value -= delta
}else{
count.value -= 1
}
}
return {
count,
increase,
decrease,
multiple
}
}
<template>
<p>count:{{count}}</p>
<p>倍数:{{multiple}}</p>
<div>
<button @click="increase(10)">加一</button>
<button @click="decrease(10)">减一</button> // 在模版中直接使用hooks中的方法作为回调函数
</div>
</template>
<script setup lang="ts">
import useCount from "../views/Hook"
const {count,multiple,increase,decrease} = useCount(10)
</script>
<style>
</style>
自定义hook需要满足的规范:
三、使用hooks的实际例子
在开发管理后台过程中,一定会遇到不少了增删改查页面,而这些页面的逻辑大多都是相同的,如获取列表数据,分页,筛选功能这些基本功能,而不同的是呈现出来的数据项,还有一些操作按钮。
对于刚开始只有 1,2 个页面的时候大多数开发者可能会直接将之前的页面代码再拷贝多一份出来,而随着项目的推进类似页面可能会越来越多,这直接导致项目代码耦合度越来越高,这也是为什么在项目中一些可复用的函数或组件要抽离出来的主要原因之一。
下面我们封装一个通用的useList
,适配大多数增删改查的列表页面,让你更快更高效的完成任务,准点下班 ~
1. 定义分页数据
export default function useList() {
const loading = ref(false);
const curPage = ref(1);
const total = ref(0);
const pageSize = ref(10);
}
2. 获取列表数据
useList
函数接收一个 listRequestFn
参数,用于请求数据。定义一个 list
变量,用于存放接口返回的数据,由于在内部无法直接确定列表数据类型,通过泛型的方式让外部提供列表数据类型。
export default function useList<ItemType extends Object>(
listRequestFn: Function
) {
const list = ref<ItemType[]>([]);
}
在 useList
中创建一个 loadData
函数,用于获取数据,该函数接收一个参数用于获取指定页数的数据;同时使用 watch
监听数据,当 curPage
,pageSize
的值发生改变时调用 loadData 函数获取新的数据。
export default function useList<ItemType extends Object>(
listRequestFn: Function
) {
const list = ref<ItemType[]>([]);
const loadData = async (page = curPage.value) => {
loading.value = true;
try {
const {
data,
meta: { total: count },
} = await listRequestFn(pageSize.value, page);
list.value = data;
total.value = count;
} catch (error) {
console.log("请求出错了", "error");
} finally {
loading.value = false;
}
};
watch([curPage, pageSize], () => {
loadData(curPage.value);
});
}
3. 实现数据筛选器
在庞大的数据列表中,数据筛选是必不可少的功能,在 useList
函数中,第二个参数接收一个 filterOption
对象,对应列表中的筛选条件字段。调整一下 loadData
函数,在请求函数中传入 filterOption 对象。注意,这里 filterOption 参数类型需要的是 ref
类型,否则会丢失响应式 无法正常工作。
export default function useList<
ItemType extends Object,
FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
const loadData = async (page = curPage.value) => {
loading.value = true;
try {
const {
data,
meta: { total: count },
} = await listRequestFn(pageSize.value, page, filterOption.value);
list.value = data;
total.value = count;
} catch (error) {
console.log("请求出错了", "error");
} finally {
loading.value = false;
}
};
}
4. 清空筛选器字段
在页面中,有一个重置的按钮,用于清空筛选条件,这个重复的动作可以交给 reset
函数处理。通过使用 Reflect
将所有值设定为 undefined
,再重新请求一次数据。
export default function useList<
ItemType extends Object,
FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
const reset = () => {
if (!filterOption.value) return;
const keys = Reflect.ownKeys(filterOption.value);
filterOption.value = {} as FilterOption;
keys.forEach((key) => {
Reflect.set(filterOption.value!, key, undefined);
});
loadData();
};
}
5. 添加导出功能
除了对数据的查看,有些界面还需要有导出数据功能(例如导出 excel 文件),此时我们也需要把导出功能写到 useList
里。通常,导出功能是调用后端提供的导出 Api
获取一个文件下载地址,和 loadData
函数类似,从外部获取 exportRequestFn
函数来调用 Api
。
export default function useList<
ItemType extends Object,
FilterOption extends Object
>(
listRequestFn: Function,
filterOption: Ref<Object>,
exportRequestFn?: Function
) {
const exportFile = async () => {
if (!exportRequestFn) {
throw new Error("当前没有提供exportRequestFn函数");
}
if (typeof exportRequestFn !== "function") {
throw new Error("exportRequestFn必须是一个函数");
}
try {
const {
data: { link },
} = await exportRequestFn(filterOption.value);
window.open(link);
} catch (error) {
console.log("导出失败", "error");
}
};
}
6. 进一步优化代码
现在,整个 useList
已经满足了页面上的需求了,拥有了获取数据,筛选数据,导出数据,分页功能;还有一些细节方面,例如在上面所有代码中的 catch
代码片段并没有做任何的处理等。
6.1 定义 Options 类型
在 useList
新增一个 Options
对象参数,用于函数成功、失败时执行指定钩子函数与输出消息内容。
export interface MessageType {
GET_DATA_IF_FAILED?: string;
GET_DATA_IF_SUCCEED?: string;
EXPORT_DATA_IF_FAILED?: string;
EXPORT_DATA_IF_SUCCEED?: string;
}
export interface OptionsType {
requestError?: () => void;
requestSuccess?: () => void;
message: MessageType;
}
export default function useList<
ItemType extends Object,
FilterOption extends Object
>(
listRequestFn: Function,
filterOption: Ref<Object>,
exportRequestFn?: Function,
options? :OptionsType
) {
}
6.2 设置Options默认值
const DEFAULT_MESSAGE = {
GET_DATA_IF_FAILED: "获取列表数据失败",
EXPORT_DATA_IF_FAILED: "导出数据失败",
};
const DEFAULT_OPTIONS: OptionsType = {
message: DEFAULT_MESSAGE,
};
export default function useList<
ItemType extends Object,
FilterOption extends Object
>(
listRequestFn: Function,
filterOption: Ref<Object>,
exportRequestFn?: Function,
options = DEFAULT_OPTIONS
) {
}
6.3 修改加载数据、导出函数
首先基于 elementui
封装 message
方法:
import { ElMessage, MessageOptions } from "element-plus";
export function message(message: string, option?: MessageOptions) {
ElMessage({ message, ...option });
}
export function warningMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: "warning" });
}
export function errorMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: "error" });
}
export function infoMessage(message: string, option?: MessageOptions) {
ElMessage({ message, ...option, type: "info" });
}
loadData 函数:
const loadData = async (page = curPage.value) => {
loading.value = true;
try {
const {
data,
meta: { total: count },
} = await listRequestFn(pageSize.value, page, filterOption.value);
list.value = data;
total.value = count;
options?.message?.GET_DATA_IF_SUCCEED &&
message(options.message.GET_DATA_IF_SUCCEED);
options?.requestSuccess?.();
} catch (error) {
options?.message?.GET_DATA_IF_FAILED &&
errorMessage(options.message.GET_DATA_IF_FAILED);
options?.requestError?.();
} finally {
loading.value = false;
}
};
exportFile 函数:
const exportFile = async () => {
if (!exportRequestFn) {
throw new Error("当前没有提供exportRequestFn函数");
}
if (typeof exportRequestFn !== "function") {
throw new Error("exportRequestFn必须是一个函数");
}
try {
const {
data: { link },
} = await exportRequestFn(filterOption.value);
window.open(link);
options?.message?.EXPORT_DATA_IF_SUCCEED &&
message(options.message.EXPORT_DATA_IF_SUCCEED);
options?.exportSuccess?.();
} catch (error) {
options?.message?.EXPORT_DATA_IF_FAILED &&
errorMessage(options.message.EXPORT_DATA_IF_FAILED);
options?.exportError?.();
}
};
7. 在组件中使用useList
<template>
<el-collapse class="mb-6">
<el-collapse-item title="筛选条件" name="1">
<el-form label-position="left" label-width="90px" :model="filterOption">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-form-item label="用户名">
<el-input
v-model="filterOption.name"
placeholder="筛选指定签名名称"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-form-item label="注册时间">
<el-date-picker
v-model="filterOption.timeRange"
type="daterange"
unlink-panels
range-separator="到"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-row class="flex mt-4">
<el-button type="primary" @click="filter">筛选</el-button>
<el-button type="primary" @click="reset">重置</el-button>
</el-row>
</el-col>
</el-row>
</el-form>
</el-collapse-item>
</el-collapse>
<el-table v-loading="loading" :data="list" border style="width: 100%">
<el-table-column label="用户名" min-width="110px">
<template #default="scope">
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column label="手机号码" min-width="130px">
<template #default="scope">
{{ scope.row.mobile || "未绑定手机号码" }}
</template>
</el-table-column>
<el-table-column label="邮箱地址" min-width="130px">
<template #default="scope">
{{ scope.row.email || "未绑定邮箱地址" }}
</template>
</el-table-column>
<el-table-column prop="createAt" label="注册时间" min-width="220px" />
<el-table-column width="200px" fixed="right" label="操作">
<template #default="scope">
<el-button type="primary" link @click="detail(scope.row)"
>详情</el-button
>
</template>
</el-table-column>
</el-table>
<div v-if="total > 0" class="flex justify-end mt-4">
<el-pagination
v-model:current-page="curPage"
v-model:page-size="pageSize"
background
layout="sizes, prev, pager, next"
:total="total"
:page-sizes="[10, 30, 50]"
/>
</div>
</template>
<script setup lang="ts">
import { UserInfoApi } from "@/network/api/User";
import useList from "@/lib/hooks/useList/index";
const filterOption = ref<UserInfoApi.FilterOptionType>({});
const {
list,
loading,
reset,
filter,
curPage,
pageSize,
reload,
total,
loadData,
} = useList<UserInfoApi.UserInfo[], UserInfoApi.FilterOptionType>(
UserInfoApi.list,
filterOption
);
</script>
作者: 前端掘金者H