记录--【vue3】写hook三天,治好了我的组件封装强迫症。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

我以前很喜欢封装组件,什么东西不喜欢别人的,总喜欢自己搞搞,这让人很有成就感,虽然是重复造轮子,但是能从无聊的crud业务中暂时解脱出来,对我来说也算是一种休息,相信有很多人跟我一样有这个习惯。 这种习惯在独立开发时无所谓,毕竟没人会关心你咋实现的,但是在跟人合作时就给别人造成了很大的困扰了,毕竟每个人封装的东西都是根据自己习惯来的,别人看着多少会有点不顺眼,而且自己封装的组件大概率也是没有写文档和注释的,所以项目其他成员的使用率也不会太高,所以今天,我试着解决这个问题。 另外,我还在一些群里看到有人抱怨vue3不如vue2好用,主要是适应不了setup写法,希望这篇博客能改变你的看法。

怎么用hook改造我的组件

关于hook是什么之类的介绍,我这就不赘述了,请看这篇文章浅谈:为啥vue和react都选择了Hooks🏂?。 前言中说到重复造轮子的组件,除开一些毫无必要的重复以外,有一些功能组件确实需要封装一下,比如说,一些需要请求后端字典到前端展示的下来选择框,点击之后要展示loading状态的按钮,带有查询条件的表单,这些非常常用的业务场景,我们就可以封装成组件,但是封装成组件就会遇到前面说的问题,每个人的使用习惯和封装习惯不一样,很难让每个人都满意,这种场景,就可以让hook来解决。

普通实现

就拿字典选择下拉框来说,如果不做封装,我们是这样写的 (这里拿ant-design-vue组件库来做示例)

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
<script setup name="DDemo" lang="ts">
  import { onMounted, ref } from 'vue';
 
  //   模拟调用接口
  function getRemoteData() {
    return new Promise<any[]>((resolve) => {
      setTimeout(() => {
        resolve([
          {
            key: 1,
            name: '苹果',
            value: 1,
          },
          {
            key: 2,
            name: '香蕉',
            value: 2,
          },
          {
            key: 3,
            name: '橘子',
            value: 3,
          },
        ]);
      }, 3000);
    });
  }
   
  const optionsArr = ref<any[]>([]);
 
  onMounted(() => {
    getRemoteData().then((data) => {
      optionsArr.value = data;
    });
  });
</script>
 
<template>
  <div>
    <a-select :options="optionsArr" />
  </div>
</template>
 
<style lang="less" scoped></style>

看起来很简单是吧,忽略我们模拟调用接口的代码,我们用在ts/js部分的代码才只有6行而已,看起来根本不需要什么封装。

但是这只是一个最简单的逻辑,不考虑接口请求超时和错误的情况,甚至都没考虑下拉框的loading表现。 如果我们把所有的意外情况都考虑到的话,代码就会变得很臃肿了。

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
<script setup name="DDemo" lang="ts">
  import { onMounted, ref } from 'vue';
 
  //   模拟调用接口
  function getRemoteData() {
    return new Promise<any[]>((resolve, reject) => {
      setTimeout(() => {
        // 模拟接口调用有概率出错
        if (Math.random() > 0.5) {
          resolve([
            {
              key: 1,
              name: '苹果',
              value: 1,
            },
            {
              key: 2,
              name: '香蕉',
              value: 2,
            },
            {
              key: 3,
              name: '橘子',
              value: 3,
            },
          ]);
        } else {
          reject(new Error('不小心出错了!'));
        }
      }, 3000);
    });
  }
 
  const optLoading = ref(false);
  const optionsArr = ref<any[]>([]);
 
  function initSelect() {
    optLoading.value = true;
    getRemoteData()
      .then((data) => {
        optionsArr.value = data;
      })
      .catch((e) => {
        // 请求出线错误时将错误信息显示到select中,给用户一个友好的提示
        optionsArr.value = [
          {
            key: -1,
            value: -1,
            label: e.message,
            disabled: true,
          },
        ];
      })
      .finally(() => {
        optLoading.value = false;
      });
  }
 
  onMounted(() => {
    initSelect();
  });
</script>
 
<template>
  <div>
    <a-select :loading="optLoading" :options="optionsArr" />
  </div>
</template>

这一次,代码直接来到了22行,虽说用户体验确实好了不少,但是这也忒费事了,而且这还只是一个下拉框,页面里有好几个下拉框也是很常见的,如此这般,可能什么逻辑都没写,页面代码就要上百行了。

这个时候,就需要我们来封装一下了,我们有两种选择:

  1. 把字典下拉框封装成一个组件
  2. 把请求、加载中、错误这些处理逻辑封装到hook里;

第一种大家都知道,就不多说了,直接说第二种

封装下拉框hook
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
import { onMounted, reactive, ref } from 'vue';
// 定义下拉框接收的数据格式
export interface SelectOption {
  value: string;
  label: string;
  disabled?: boolean;
  key?: string;
}
// 定义入参格式
interface FetchSelectProps {
  apiFun: () => Promise<any[]>;
}
 
export function useFetchSelect(props: FetchSelectProps) {
  const { apiFun } = props;
 
  const options = ref<SelectOption[]>([]);
 
  const loading = ref(false);
 
  /* 调用接口请求数据 */
  const loadData = () => {
    loading.value = true;
    options.value = [];
    return apiFun().then(
      (data) => {
        loading.value = false;
        options.value = data;
        return data;
      },
      (err) => {
        // 未知错误,可能是代码抛出的错误,或是网络错误
        loading.value = false;
        options.value = [
          {
            value: '-1',
            label: err.message,
            disabled: true,
          },
        ];
        // 接着抛出错误
        return Promise.reject(err);
      }
    );
  };
 
  //   onMounted 中调用接口
  onMounted(() => {
    loadData();
  });
 
  return reactive({
    options,
    loading,
  });
}

然后在组件中调用

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
<script setup name="DDemo" lang="ts">
  import { useFetchSelect } from './hook';
 
  //   模拟调用接口
  function getRemoteData() {
    return new Promise<any[]>((resolve, reject) => {
      setTimeout(() => {
        // 模拟接口调用有概率出错
        if (Math.random() > 0.5) {
          resolve([
            {
              key: 1,
              name: '苹果',
              value: 1,
            },
            {
              key: 2,
              name: '香蕉',
              value: 2,
            },
            {
              key: 3,
              name: '橘子',
              value: 3,
            },
          ]);
        } else {
          reject(new Error('不小心出错了!'));
        }
      }, 3000);
    });
  }
    
   // 将之前用的 options,loading,和调用接口的逻辑都抽离到hook中
  const selectBind = useFetchSelect({
    apiFun: getRemoteData,
  });
</script>
 
<template>
  <div>
    <!-- 将hook返回的接口,通过 v-bind 绑定给组件 -->
    <a-select v-bind="selectBind" />
  </div>
</template>

这样一来,代码行数直接又从20行降到3行,甚至比刚开始最简单的那个还要少两行,但是功能却一点不少,用户体验也是比较完善的。

如果你觉着上面这个例子不能打动你的话,可以看看下面这个

Loading状态hook

点击按钮,调用接口是另一个我们经常遇到的场景,为了更好的用户体验,提示用户操作已经响应,同时防止用户多次点击,我们要在调用接口的同时将按钮置为loading状态,虽说只有一个loading状态,但是写多了也觉着麻烦。

为此我们可以封装一个非常简单的hook:

hook.ts

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
import { Ref, ref } from 'vue';
 
type TApiFun<TData, TParams extends Array<any>> = (...params: TParams) => Promise<TData>;
 
interface AutoRequestOptions {
   // 定义一下初始状态
  loading?: boolean;
  // 接口调用成功时的回调
  onSuccess?: (data: any) => void;
}
 
type AutoRequestResult<TData, TParams extends Array<any>> = [Ref<boolean>, TApiFun<TData, TParams>];
 
/* 控制loading状态的自动切换hook */
export function useAutoRequest<TData, TParams extends any[] = any[]>(fun: TApiFun<TData, TParams>, options?: AutoRequestOptions): AutoRequestResult<TData, TParams> {
  const { loading = false, onSuccess } = options || { loading: false };
 
  const requestLoading = ref(loading);
 
  const run: TApiFun<TData, TParams> = (...params) => {
    requestLoading.value = true;
    return fun(...params)
      .then((res) => {
        onSuccess && onSuccess(res);
        return res;
      })
      .finally(() => {
        requestLoading.value = false;
      });
  };
 
  return [requestLoading, run];
}

这次把模拟接口的方法单独抽出一个文件

api/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function submitApi(text: string) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟接口调用有概率出错
      if (Math.random() > 0.5) {
        resolve({
          status: "ok",
          text: text,
        });
      } else {
        reject(new Error("不小心出错了!"));
      }
    }, 3000);
  });
}

使用:

index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup name="Index" lang="ts">
import { useAutoRequest } from "./hook";
import { Button } from "ant-design-vue";
import { submitApi } from "@/api";
 
const [loading, submit] = useAutoRequest(submitApi);
 
function onSubmit() {
   submit("aaa").then((res) => {
    console.log("res", res);
  });
}
</script>
 
<template>
  <div class="col">
    <Button :loading="loading" @click="onSubmit">提交</Button>
  </div>
</template>

这样封装一下,我们使用时就不再需要手动切换loading的状态了。

这个hook还有另一种玩法:

hook2.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import type { Ref } from "vue";
import { ref } from "vue";
 
type AutoLoadingResult = [
  Ref<boolean>,
  <T>(requestPromise: Promise<T>) => Promise<T>
];
 
/* 在给run方法传入一个promise,会在promise执行前或执行后将loading状态设为true,在执行完成后设为false */
export function useAutoLoading(defaultLoading = false): AutoLoadingResult {
  const ld = ref(defaultLoading);
 
  function run<T>(requestPromise: Promise<T>): Promise<T> {
    ld.value = true;
    return requestPromise.finally(() => {
      ld.value = false;
    });
  }
 
  return [ld, run];
}

使用:

index.vue

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
<script setup name="Index" lang="ts">
// import { useAutoRequest } from "./hook";
import { useAutoLoading } from "./hook2";
import { Button } from "ant-design-vue";
import { submitApi, cancelApi } from "@/api";
 
// const [loading, submit] = useAutoRequest(submitApi);
 
const [commonLoading, fetch] = useAutoLoading();
 
function onSubmit() {
  fetch(submitApi("submit")).then((res) => {
    console.log("res", res);
  });
}
 
function onCancel() {
  fetch(cancelApi("cancel")).then((res) => {
    console.log("res", res);
  });
}
</script>
 
<template>
  <div class="col">
    <Button type="primary" :loading="commonLoading" @click="onSubmit">
      提交
    </Button>
    <Button :loading="commonLoading" @click="onCancel">取消</Button>
  </div>
</template>

这里也是用到了promise链式调用的特性,在接口调用之后马上将loading置为true,在接口调用完成后置为false。而useAutoRequest则是在接口调用之前就将loading置为true。

useAutoRequest调用时代码更简洁,useAutoLoading的使用则更灵活,可以同时服务给多个接口使用,比较适合提交取消这种互斥的场景。

解放组件

如果你翻看过我的这篇博客一个省心省力的骨架屏实现方案,那么肯定知道在骨架屏组件中,我是用了传入的res对象的code属性来判断当前显示的视图状态。长话短说就是, res是接口返回给前端的数据,如

1
2
3
4
5
6
7
8
{
    "code":0,
    "msg":'查询成功',
    "data":{
        "username":"小王",
        "age":20,
    }
}

我们假定当code0时代表成功,不为0表示失败,为-100时表示正在加载,当然接口并不会也不需要返回-100-100是我们本地捏造出来的,只是为了让骨架屏组件显示对应的加载状态。 在页面中使用时,我们需要先声明一个code-100res对象绑定给骨架屏组件,然后在onMounted中调用查询接口,调用成功后更新res对象。

如果像上面这样使用res对象来给骨架屏组件设置状态的话,就感觉非常的麻烦,有时候我们只是要设置一个初始时的加载状态,但是要搞好几行没用的代码,但是如果我们把res拆解成一个个参数单独传递的话,父组件需要维护的变量就会非常多了,这时我们就可以封装hook来解决这个问题,把拆解出来的参数都扔到hook里面保存。

上代码(这部分代码比较长,想要详细了解的话可以去看原文章)

骨架屏组件

SkeletonView/index.vue

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<script setup lang="ts">
import { defineProps, computed } from "vue";
import { LoadingOutlined } from "@ant-design/icons-vue";
import { isArray } from "@/utils/is";
import { Button } from "ant-design-vue";
 
/* status:'loading','error','success','empty' */
type ViewStatus = "loading" | "error" | "success" | "empty";
 
interface SkeletonProps<T = any> {
  status: ViewStatus;
  result: T;
  placeholderResult: T;
  emptyMsg?: string;
  errorMsg?: string;
  isEmpty?: (result: T) => boolean;
}
 
const props = withDefaults(defineProps<SkeletonProps>(), {
  status: "loading",
  emptyMsg: "暂无数据",
  errorMsg: "未知错误",
});
 
const emits = defineEmits(["retry"]);
 
const retryClick = () => {
  emits("retry");
};
 
const viewStatus = computed(() => {
  const status = props.status;
 
  if (status === "success") {
    let isEmp = false;
    const result = props.result;
    if (props.isEmpty) {
      isEmp = props.isEmpty(props.result);
    } else {
      if (isArray(result)) {
        isEmp = result.length === 0;
      } else if (!result) {
        isEmp = true;
      } else {
        isEmp = false;
      }
    }
    if (isEmp) {
      return "empty";
    }
    return "success";
  }
  return status;
});
 
const placeholderData = computed(() => {
  if (props.result) {
    return props.result;
  }
  return props.placeholderResult;
});
</script>
 
<template>
  <div v-if="viewStatus === 'empty'" key="empty" class="empty_view flex-col">
    <span>{{ emptyMsg }}</span>
    <Button class="mt4 max-w-160px" @click="retryClick">重试</Button>
  </div>
 
  <div
    key="error"
    v-else-if="viewStatus === 'error'"
    class="empty_view flex-col"
  >
    <span>{{ errorMsg }}</span>
    <Button class="mt4 max-w-160px" @click="retryClick">重试</Button>
  </div>
 
  <div
    v-else
    key="loadingOrContent"
    :class="[
      placeholderData && viewStatus === 'loading'
        ? 'skeleton-view-empty-view'
        : 'skeleton-view-default-view',
    ]"
  >
    <div
      v-if="!placeholderData && viewStatus === 'loading'"
      class="loading-center"
    >
      <LoadingOutlined style="font-size: 40px; color: #2a6de5" />
    </div>
    <slot
      v-else
      :result="placeholderData"
      :status="viewStatus"
      :success="viewStatus === 'success'"
      :mask="viewStatus === 'loading' ? 'skeleton-mask' : ''"
    ></slot>
  </div>
</template>
 
<style>
.clam-box {
  width: 100%;
  height: 100%;
}
.empty_view {
  padding-top: 50px;
  padding-bottom: 50px;
  align-items: center;
}
.empty_img {
  width: 310px;
  height: 218px;
}
.trip_text {
  font-size: 20px;
  color: #999999;
}
 
.mt4 {
  margin-top: 4px;
}
 
.flex-col {
  display: flex;
  flex-direction: column;
}
 
.loading-center {
  padding: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
}
 
.skeleton-view-default-view span,
.skeleton-view-default-view a,
.skeleton-view-default-view img,
.skeleton-view-default-view td,
.skeleton-view-default-view button {
  transition-duration: 0.7s;
  transition-timing-function: ease;
  transition-property: background, width;
}
 
.skeleton-view-empty-view {
  position: relative;
  pointer-events: none;
}
 
.skeleton-view-empty-view::before {
  content: " ";
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  background: linear-gradient(
    110deg,
    rgba(255, 255, 255, 0.1) 40%,
    rgba(180, 199, 255, 0.3) 50%,
    rgba(255, 255, 255, 0.1) 60%
  );
  background-size: 200% 100%;
  background-position-x: 180%;
  animation: loading 1s ease-in-out infinite;
  z-index: 1;
}
 
@keyframes loading {
  to {
    background-position-x: -20%;
  }
}
 
.skeleton-view-empty-view .skeleton-mask {
  position: relative;
}
.skeleton-view-empty-view .skeleton-mask::before {
  content: " ";
  background-color: #f5f5f5;
  position: absolute;
  width: 100%;
  height: 100%;
  border: 1px solid #f5f5f5;
  top: -1px;
  left: -1px;
  z-index: 1;
}
 
.skeleton-view-empty-view button,
.skeleton-view-empty-view span,
.skeleton-view-empty-view input,
.skeleton-view-empty-view td,
.skeleton-view-empty-view a {
  color: rgba(0, 0, 0, 0) !important;
  border: none;
  background: #f5f5f5 !important;
}
/* [src=""],img:not([src])*/
.skeleton-view-empty-view img {
  content: url(./no_url.png);
  border-radius: 2px;
  background: #f5f5f5 !important;
}
</style>

使用 index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup name="SkeletonView" lang="ts">
import SkeletonView from "@/components/SkeletonView/index.vue";
import { useAutoSkeletonView } from "./useAutoSkeletonView";
import { listApi } from "@/api";
 
const view = useAutoSkeletonView({
  apiFun: listApi,
});
</script>
 
<template>
  <div class="col">
    <SkeletonView
      v-slot="{ result }"
      v-bind="view.bindProps"
      v-on="view.bindEvents"
    >
      <span>{{ result }}</span>
    </SkeletonView>
  </div>
</template>

这里的SkeletonView不光用v-bind绑定了hook抛出的属性,还用v-on绑定的事件,目的就是监听请求报错时出现的“重试”按钮的点击事件。

使用优化

经常写react的朋友可能早就看出来了,这不是跟react中的一部分hook用法如出一辙吗?没错,很多人写react就这么写,而且react中绑定hook跟组件更简单,只需要...就可以了,比如:

1
2
3
4
5
6
7
function Demo(){
    const select = useSelect({
        apiFun:getDict
    })
    // 这里可以直接用...将useSelect返回的属性与方法全部绑定给Select组件
    return <Select {...select}>;
}

比起vuev-bindv-on算是简便了不少。那么,有没有一种办法也能做到差不多的效果呢?就比如能做到v-xxx="select"

博主首先想到的就是vue的自定义指令了,文档在这里,但是折腾了半天发现行不通,因为自定义指令主要还是针对dom来的。vue官网原话:

总的来说,推荐在组件上使用自定义指令。

那么就只能考虑打包插件了,只要我们在vue解析template之前把v-xxx="select"翻译成v-bind="select.bindProps" v-on="select.bindEvents" 就好了,听起来并不难,只要我们开发的时候规定绑定组件的hook返回格式必须有bindPropsbindEvents就好了。

思路有了,直接开干,现在vue官网的默认创建方式也改成vite,我们就直接写vite的插件(不想看可以跳到最后用现成的):

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
// component-enhance-hook
import type { PluginOption } from "vite";
 
// 可以自定义hook绑定的前缀、绑定的属性值合集对应的键和事件合集对应的键
type HookBindPluginOptions = {
  prefix?: string;
  bindKey?: string;
  eventKey?: string;
};
export const viteHookBind = (options?: HookBindPluginOptions): PluginOption => {
  const { prefix, bindKey, eventKey } = Object.assign(
    {
      prefix: "v-ehb",
      bindKey: "bindProps",
      eventKey: "bindEvents",
    },
    options
  );
 
  return {
    name: "vite-plugin-vue-component-enhance-hook-bind",
    enforce: "pre",
    transform: (code, id) => {
      const last = id.substring(id.length - 4);
 
      if (last === ".vue") {
        // 处理之前先判断一下
        if (code.indexOf(prefix) === -1) {
          return code;
        }
        // 获取 template 开头
        const templateStrStart = code.indexOf("<template>");
        // 获取 template 结尾
        const templateStrEnd = code.lastIndexOf("</template>");
 
        let templateStr = code.substring(templateStrStart, templateStrEnd + 11);
 
        let startIndex;
        // 循环转换 template 中的hook绑定指令
        while ((startIndex = templateStr.indexOf(prefix)) > -1) {
          const endIndex = templateStr.indexOf(`"`, startIndex + 7);
          const str = templateStr.substring(startIndex, endIndex + 1);
          const obj = str.split(`"`)[1];
 
          const newStr = templateStr.replace(
            str,
            `v-bind="${obj}.${bindKey}" v-on="${obj}.${eventKey}"`
          );
 
          templateStr = newStr;
        }
 
        // 拼接并返回
        return (
          code.substring(0, templateStrStart) +
          templateStr +
          code.substring(templateStrEnd + 11)
        );
      }
 
      return code;
    },
  };
};

应用插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { fileURLToPath, URL } from "node:url";
 
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
 
import { viteHookBind } from "./vBindPlugin";
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), vueJsx(), viteHookBind()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

修改一下vue中的用法

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
<script setup name="SkeletonView" lang="ts">
import SkeletonView from "@/components/SkeletonView/index.vue";
import { useAutoSkeletonView } from "./useAutoSkeletonView";
import { listApi } from "@/api";
 
const view = useAutoSkeletonView({
  queryInMount: true,
  apiFun: listApi,
  placeholderResult: [
    {
      key: 1,
      name: "苹果",
      value: 1,
    },
    {
      key: 2,
      name: "香蕉",
      value: 2,
    },
    {
      key: 3,
      name: "橘子",
      value: 3,
    },
  ],
});
</script>
 
<template>
  <div class="col">
    <SkeletonView v-slot="{ result }" v-ehb="view">
      <span>{{ result }}</span>
    </SkeletonView>
  </div>
</template>

OK! 完成了!

使用npm安装

不过我也提前打包编译好了发布在了npm上,需要的话可以直接使用这个

1
npm i vite-plugin-vue-hook-enhance -D

改一下引入方式就可以了

1
import { viteHookBind } from "vite-plugin-vue-hook-enhance";

本文转载于:

https://juejin.cn/post/7181712900094951483

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

posted @   林恒  阅读(635)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
欢迎阅读『记录--【vue3】写hook三天,治好了我的组件封装强迫症。』
点击右上角即可分享
微信分享提示