赞
踩
我以前很喜欢封装组件,什么东西不喜欢别人的,总喜欢自己搞搞,这让人很有成就感,虽然是重复造轮子,但是能从无聊的crud业务中暂时解脱出来,对我来说也算是一种休息,相信有很多人跟我一样有这个习惯。 这种习惯在独立开发时无所谓,毕竟没人会关心你咋实现的,但是在跟人合作时就给别人造成了很大的困扰了,毕竟每个人封装的东西都是根据自己习惯来的,别人看着多少会有点不顺眼,而且自己封装的组件大概率也是没有写文档和注释的,所以项目其他成员的使用率也不会太高,所以今天,我试着解决这个问题。
另外,我还在一些群里看到有人抱怨vue3不如vue2好用,主要是适应不了setup写法,希望这篇博客能改变你的看法。
前言中说到重复造轮子的组件,除开一些毫无必要的重复以外,有一些功能组件确实需要封装一下,比如说,一些需要请求后端字典到前端展示的下来选择框,点击之后要展示loading状态的按钮,带有查询条件的表单,这些非常常用的业务场景,我们就可以封装成组件,但是封装成组件就会遇到前面说的问题,每个人的使用习惯和封装习惯不一样,很难让每个人都满意,这种场景,就可以让hook来解决。
就拿字典选择下拉框来说,如果不做封装,我们是这样写的 (这里拿ant-design-vue组件库来做示例)
- <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
表现。 如果我们把所有的意外情况
都考虑到的话,代码就会变得很臃肿了。
- <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
行,虽说用户体验确实好了不少,但是这也忒费事了,而且这还只是一个下拉框,页面里有好几个下拉框也是很常见的,如此这般,可能什么逻辑都没写,页面代码就要上百行了。
这个时候,就需要我们来封装一下了,我们有两种选择:
把字典下拉框封装成一个组件
;
把请求、加载中、错误这些处理逻辑封装到hook
里;
第一种大家都知道,就不多说了,直接说第二种
- 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,
- });
- }
-
然后在组件中调用
- <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
状态,虽说只有一个loading状态,但是写多了也觉着麻烦。
为此我们可以封装一个非常简单的hook:
hook.ts
- 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
- 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
- <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
- 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
- <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
是接口返回给前端的数据,如
- {
- "code":0,
- "msg":'查询成功',
- "data":{
- "username":"小王",
- "age":20,
- }
- }
-
我们假定当code
为0
时代表成功,不为0
表示失败,为-100
时表示正在加载,当然接口并不会也不需要返回-100
,-100
是我们本地捏造出来的,只是为了让骨架屏组件显示对应的加载状态。 在页面中使用时,我们需要先声明一个code
为-100
的res
对象绑定给骨架屏组件,然后在onMounted
中调用查询接口,调用成功后更新res
对象。
如果像上面这样使用res
对象来给骨架屏组件设置状态的话,就感觉非常的麻烦,有时候我们只是要设置一个初始时的加载状态,但是要搞好几行没用的代码,但是如果我们把res
拆解成一个个参数单独传递的话,父组件需要维护的变量就会非常多了,这时我们就可以封装hook来解决这个问题,把拆解出来的参数都扔到hook里面保存。
上代码(这部分代码比较长,想要详细了解的话可以去看原文章)
SkeletonView/index.vue
- <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>
-
-
这里样式中用到的no_url.png
只是一张空白透明图片,防止加载时图片显示裂图。
hook代码 useAutoSkeletonView.ts
- import { computed, onMounted, reactive, ref } from "vue";
- import type { UnwrapRef } from "vue";
-
- type TApiFun<TData, TParams extends Array<any>> = (
- ...params: TParams
- ) => Promise<TData>;
-
- /* 定义可自定义的默认状态 */
- export type SkeletonStatus = "loading" | "success";
-
- export interface IUseAutoSkeletonViewProps<TData, TParams extends any[]> {
- apiFun: TApiFun<TData, TParams>;// 调用接口api
- placeholderResult?: TData; // 骨架屏用到的占位数据
- queryInMount?: boolean; // 在父组件挂载时自动调用接口,默认true
- initQueryParams?: TParams; // 调用接口用到的参数
- transformDataFun?: (data: TData) => TData; // 接口请求完成后,转换数据
- updateParamsOnFetch?: boolean; // 手动调用接口后,更新请求参数
- defaultStatus?: SkeletonStatus; // 默认骨架屏组件状态
- onSuccess?: (data: any) => void; // 接口调用成功的回调
- isEmpty?: (data: TData) => boolean; // 重写骨架屏判空逻辑
- }
-
- export type IAutoSkeletonViewResult<TData, TParams extends any[]> = UnwrapRef<{
- execute: TApiFun<TData, TParams>;
- result: TData | null;
- retry: () => Promise<TData>;
- loading: boolean;
- status: SkeletonStatus | "error";
- getField: (key: string) => any;
- bindProps: {
- result: TData | null;
- status: SkeletonStatus | "error";
- errorMsg: string;
- placeholderResult?: TData;
- isEmpty?: (data: TData) => boolean;
- };
- bindEvents: {
- retry: () => Promise<TData>;
- };
- }>;
-
- export function useAutoSkeletonView<TData = any, TParams extends any[] = any[]>(
- prop: IUseAutoSkeletonViewProps<TData, TParams>
- ): IAutoSkeletonViewResult<TData, TParams> {
- const {
- apiFun,
- defaultStatus = "loading",
- placeholderResult,
- isEmpty,
- initQueryParams = [],
- transformDataFun,
- onSuccess,
- updateParamsOnFetch = true,
- queryInMount = true,
- } = prop;
-
- const status = ref<SkeletonStatus | "error">(defaultStatus);
-
- const result = ref<TData | null>(null);
-
- const placeholder = ref<TData | undefined>(placeholderResult);
-
- const errorMsg = ref("");
-
- const lastFetchParams = ref<TParams>(initQueryParams as TParams);
-
- const executeApiFun: TApiFun<TData, TParams> = (...params: TParams) => {
- if (updateParamsOnFetch) {
- lastFetchParams.value = params;
- }
-
- status.value = "loading";
-
- return apiFun(...params)
- .then((res) => {
- let data: any = res;
- if (transformDataFun) {
- data = transformDataFun(res);
- }
- placeholder.value = data;
- result.value = data;
- status.value = "success";
- onSuccess && onSuccess(data);
- return res;
- })
- .catch((e) => {
- console.error("--useAutoSkeletonView--", e);
- status.value = "error";
- errorMsg.value = e.message;
- throw e;
- });
- };
-
- function retry() {
- return executeApiFun(...(lastFetchParams.value as TParams));
- }
-
- onMounted(() => {
- if (queryInMount && defaultStatus === "loading") {
- executeApiFun(...(initQueryParams as TParams));
- }
- });
-
- const loading = computed(() => {
- return status.value === "loading";
- });
-
- function getField(key: string) {
- if (status.value !== "success") {
- return "";
- }
- if (result.value) {
- // @ts-ignore
- return result.value[key];
- }
- return "";
- }
-
- return reactive({
- execute: executeApiFun,
- result: result,
- retry,
- loading,
- status,
- getField,
- bindProps: {
- result: result,
- status,
- errorMsg,
- placeholderResult: placeholder,
- isEmpty,
- },
- bindEvents: {
- retry: retry,
- },
- });
- }
-
使用 index.vue
- <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跟组件更简单,只需要...就可以了,比如:
- function Demo(){
- const select = useSelect({
- apiFun:getDict
- })
- // 这里可以直接用...将useSelect返回的属性与方法全部绑定给Select组件
- return <Select {...select}>;
- }
-
比起vue
的v-bind
和v-on
算是简便了不少。那么,有没有一种办法也能做到差不多的效果呢?就比如能做到v-xxx="select"
。
博主首先想到的就是vue的自定义指令了,文档在这里(https://cn.vuejs.org/guide/reusability/custom-directives.html),但是折腾了半天发现行不通,因为自定义指令主要还是针对dom来的。vue官网原话:
总的来说,不推荐在组件上使用自定义指令。
那么就只能考虑打包插件了,只要我们在vue
解析template
之前把v-xxx="select"
翻译成v-bind="select.bindProps" v-on="select.bindEvents"
就好了,听起来并不难,只要我们开发的时候规定绑定组件的hook返回格式必须有bindProps
和bindEvents
就好了。
思路有了,直接开干,现在vue
官网的默认创建方式也改成vite,我们就直接写vite的插件(不想看可以跳到最后用现成的):
- // 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;
- },
- };
- };
-
应用插件
- 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中的用法
- <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 i vite-plugin-vue-hook-enhance \-D
改一下引入方式就可以了
import { viteHookBind } from "vite-plugin-vue-hook-enhance";
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。