vue3技巧

vue3 技巧

1. 组件自动注册

在项目中有很多通用组件需要重复导入,以前的做法是在 main.js 中一个一个引入,然后注册为全局组件。现借助于 vite,我们完全可以这样做

1
2
3
4
5
6
7
8
9
// vite.config.js
import Components from "unplugin-vue-components/vite";
export default {
plugins: [
Components({
dirs: ["src/components"], // 指定组件目录
}),
],
};

不用在每个文件手动导入相同组件,通过 Vite 插件自动扫描 components 目录,直接在模板中使用组件,省掉 import 语句。

2.图片懒加载自定义指令

在一些重效果的项目中,首页会有很多的图片需要加载,如果一进入就开始加载,可能会导致页面展示的滞后。可以封装一个图片懒加载指令,减少首屏首页的加载时间,利用 IntersectionObserver 监听元素进入视口:

1
2
3
4
5
6
7
8
9
10
11
app.directive("lazy", {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
el.src = binding.value; // 图片进入视口时加载
observer.unobserve(el);
}
});
observer.observe(el);
},
});

注意一定要设置图片的占位符,防止页面出现抖动。

3.动态 CSS 变量,动态 Class

在表单验证、交互反馈 等一些场景中,动态去绑定 css 变量和 class 类名是很实用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 动态css变量 -->
<template>
<div
:style="{
'--theme-color': themeColor, // JS变量转为CSS变量
'--size': size + 'px',
}"
>
<div class="box"></div>
</div>
</template>

<style>
.box {
background: var(--theme-color); /* 使用CSS变量 */
width: var(--size);
}
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 动态class类名 -->
<script setup>
const isValid = ref(false); // 响应式状态
</script>

<template>
<!-- 根据状态添加error类 -->
<input :class="{ error: !isValid }" />
</template>

<style>
input.error {
border: 2px solid red; /* 错误状态样式 */
}
</style>

4.Suspense“加载协调员”的巧妙调度

在 Vue3 应用时,应该遇到过这样的尴尬情况异步组件还没加载完,页面突然白屏,用户一脸懵,体验感直线下降。而 Suspense 这位“加载协调员”能帮你巧妙化解这个难题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<Suspense>
<!-- 异步组件加载成功后展示的内容 -->
<template #default> <AsyncComponent /> </template>
<!-- 异步组件加载过程中展示的占位内容 -->
<template #fallback>
<div class="loading-spinner">努力加载中,别走开~</div>
</template>
</Suspense>
</template>

<script>
import { defineAsyncComponent, Suspense } from "vue"; // 定义异步组件,只有在使用时才会加载对应的代码
const AsyncComponent = defineAsyncComponent(() =>
import("./AsyncComponent.vue")
);
export default { components: { Suspense, AsyncComponent } };
</script>

5.组件传送到指定位置

有时候,我们会遇到这样的需求:一个组件的逻辑在某个地方,但它的渲染位置却要在其他地方,比如模态框、下拉菜单等。Vue3 的 Teleport 组件就能轻松实现这个功能。

1
2
3
4
5
6
7
8
9
10
<template>
<!-- 将内容渲染到body末尾 -->
<Teleport to="body">
<div class="modal" v-if="showModal"> 模态框内容 </div>
</Teleport>
</template>

<script setup>
const showModal = ref(false);
</script>

这样很好的解决了父组件 overflow:hidden 导致模态框被裁剪的一些情况。

6.组件缓存

在标签页切换的时候,一个页面需要导入多个组件,这个时候,如果不希望切换的时候,将组件初始化,可以利用 KeepAlive 组件来实现想要的效果。

1
2
3
4
5
6
7
8
9
10
11
<template>
<!-- 缓存指定组件 -->
<KeepAlive include="TabA,TabB">
<component :is="currentComponent" />
</KeepAlive>
</template>

<script setup>
import { ref } from "vue";
const currentComponent = ref("TabA");
</script>

配置选项:

  • include:指定缓存组件
  • max:最大缓存实例数

7.用 VueUse 库提升开发效率

**https://vueuse.pages.dev/**

VueUse 是一个非常强大的 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
// 引入 VueUse 中的 useIntersectionObserver 函数
import { useIntersectionObserver } from "@vueuse/core";
import { ref } from "vue";
export default {
setup() {
// 定义一个 ref 来引用 DOM 元素
const target = ref(null);
// 定义一个 ref 来保存元素是否可见的状态
const isVisible = ref(false);
// 使用 useIntersectionObserver 函数监听元素的可见性
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }]) => {
// 当元素可见时,更新 isVisible 的值
isVisible.value = isIntersecting;
if (isIntersecting) {
// 当元素可见时,可以执行一些操作,比如加载数据
console.log("元素可见了");
// 停止监听
stop();
}
},
{
// 配置项,threshold 表示元素可见的比例
threshold: 0.5,
}
);
return {
target,
isVisible,
};
},
template: `
<div ref="target">
<!-- 根据元素的可见状态显示不同的内容 -->
<p v-if="isVisible">元素现在可见啦</p>
<p v-else>元素还没进入可视区域哦</p>
</div>
`,
};

VueUse 就像是一个百宝箱,里面有很多实用工具。在上面例子中,使用了 useIntersectionObserver 函数来监听元素的可见性,当元素进入可视区域时,就可以执行一些操作,比如加载数据。这对于实现懒加载、无限滚动等功能非常有用。

8. 防抖元素

vue2 版本

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
//  my-throttle.js

/**
*
* Renderless 事件节流/防抖包装组件(Vue2)。
* 作用:不改变子节点结构,仅在渲染时劫持并替换其事件回调,实现统一的节流/防抖。
* 使用示例:
* <my-throttle :time="500" :throttle="true">
* <el-button @click="onClick">提交</el-button>
* </my-throttle>
* 参数:
* - time(Number):时间窗,单位毫秒,默认 1000。
* - throttle(Boolean):true=节流,false=防抖。
* 依赖:baseMethod 需提供 debounce 和 throttle 两个方法。
*/

export default {
name: "my-throttle",
abstract: true,
props: {
time: {
type: Number,
default: 1000,
},
throttle: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
render() {
// 提示:Vue2 的 $slots 在 Vue3 中以函数形式暴露在 $scopedSlots,这里兼容 Vue2 写法。
// 仅处理默认插槽的第一个子节点:若存在多个子节点,建议外部包一层。
const vnode = this.$slots.default ? this.$slots.default[0] : null;

if (vnode) {
let throttledMap = {}; // 缓存被节流/防抖包裹后的事件处理器
let listeners = vnode.data.on || vnode.componentOptions.listeners; // 原生元素事件或子组件事件
let _evnet = this.throttle ? "throttle" : "debounce"; // 根据 props.throttle 选择策略(变量名有拼写问题,但沿用原逻辑)
if (listeners) {
// 遍历事件名(如 'click'、'input' 等),逐一包裹处理器
Object.getOwnPropertyNames(listeners).forEach((key) => {
if (!throttledMap[key]) {
// 通过 baseMethod 的 throttle/debounce 包裹原处理器,传入 time 与 defer
throttledMap[key] = baseMethod[_evnet](listeners[key], this.time);
}
});
// 用新包裹后的处理器替换原有 listeners
Object.getOwnPropertyNames(throttledMap).forEach((key) => {
listeners[key] = throttledMap[key];
});
}
}
return vnode; // 返回处理后的 vnode,结构不变,仅修改事件行为
},
};
1
2
3
4
5
6
7
8
// ...temeplate.vue
<temeplate>
<div>
<my-throttle>
<button @click="testThrottle">防抖按钮</button>
</my-throttle>
</div>
</temeplate>

Vue3

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
//  throttle.ts

// throttle: 一个轻量包装组件,用于给子节点的事件(on*)统一加防抖/节流
// 彻底修复:完全避免cloneVNode,使用h函数重新创建VNode,确保事件不会重复触发
// 优化:处理默认插槽返回的所有子节点,并递归处理其子数组,保持实现简洁
import { defineComponent, h, type VNode } from "vue";
import baseMethods from "../utils/baseMethods";

type EventHandler = (...args: any[]) => any;

export default defineComponent({
name: "throttle",
props: {
// 单位时间(毫秒),用于防抖/节流的时间窗口
time: { type: Number, default: 1000 },
// true=节流;false=防抖
throttle: { type: Boolean, default: false },
// 防抖/节流的 defer 行为:根据 baseMethods 的实现
// 一般情况下:defer=true 表示"先执行一次,后续在窗口内抑制",与常见防抖略有不同
defer: { type: Boolean, default: true },
},
setup(props, { slots }) {
return () => {
// 处理默认插槽的所有子节点;无子节点时返回 null
const children = slots.default?.() ?? [];
if (!children.length) return null;

const method = props.throttle ? "throttle" : "debounce";

const createWrappedHandler = (handler: EventHandler): EventHandler => {
return baseMethods[method](handler, props.time, props.defer);
};

const wrapEventHandlers = (originalProps: Record<string, any>) => {
const wrappedProps: Record<string, any> = {};

// 复制所有属性,对事件属性进行包装
for (const key in originalProps) {
const val = originalProps[key];

if (key.startsWith("on") && val) {
// 对事件属性进行包装处理
if (Array.isArray(val)) {
wrappedProps[key] = (val as EventHandler[]).map(
createWrappedHandler
);
} else if (typeof val === "function") {
wrappedProps[key] = createWrappedHandler(val as EventHandler);
} else {
// 非函数的事件属性直接复制
wrappedProps[key] = val;
}
} else {
// 非事件属性直接复制
wrappedProps[key] = val;
}
}

return wrappedProps;
};

const processVNode = (node: any): any => {
// 跳过非VNode对象(文本节点、注释节点等)
if (!node || typeof node !== "object" || !node.type) {
return node;
}

const originalProps: Record<string, any> = node.props || {};

// 检查是否有事件属性需要处理
const hasEventProps = Object.keys(originalProps).some(
(key) =>
key.startsWith("on") && typeof originalProps[key] === "function"
);

// 递归处理子节点
let processedChildren = node.children;
if (Array.isArray(node.children)) {
processedChildren = node.children.map((c: any) => processVNode(c));
}

if (!hasEventProps) {
// 没有事件属性,使用h函数重新创建VNode但保持原始props
return h(node.type, originalProps, processedChildren);
}

// 有事件属性,使用包装后的props重新创建VNode
const wrappedProps = wrapEventHandlers(originalProps);
return h(node.type, wrappedProps, processedChildren);
};

return children.map((c) => processVNode(c));
};
},
});
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
// throttle.js

// throttle: 一个轻量包装组件,用于给子节点的事件(on*)统一加防抖/节流
// 彻底修复:完全避免cloneVNode,使用h函数重新创建VNode,确保事件不会重复触发
// 优化:处理默认插槽返回的所有子节点,并递归处理其子数组,保持实现简洁
import { defineComponent, h } from "vue";
import baseMethods from "../utils/baseMethods";

export default defineComponent({
name: "throttle",
**props: {
// 单位时间(毫秒),用于防抖/节流的时间窗口
time: { type: Number, default: 1000 },
// true=节流;false=防抖
throttle: { type: Boolean, default: false },
// 防抖/节流的 defer 行为:根据 baseMethods 的实现
// 一般情况下:defer=true 表示"先执行一次,后续在窗口内抑制",与常见防抖略有不同
defer: { type: Boolean, default: true },
},
setup(props, { slots }) {
return () => {
// 处理默认插槽的所有子节点;无子节点时返回 null
const children = slots.default?.() ?? [];
if (!children.length) return null;

const method = props.throttle ? "throttle" : "debounce";

const createWrappedHandler = (handler) => {
return baseMethods[method](handler, props.time, props.defer);
};

const wrapEventHandlers = (originalProps) => {
const wrappedProps = {};

// 复制所有属性,对事件属性进行包装
for (const key in originalProps) {
const val = originalProps[key];

if (key.startsWith("on") && val) {
// 对事件属性进行包装处理
if (Array.isArray(val)) {
wrappedProps[key] = val.map(createWrappedHandler);
} else if (typeof val === "function") {
wrappedProps[key] = createWrappedHandler(val);
} else {
// 非函数的事件属性直接复制
wrappedProps[key] = val;
}
} else {
// 非事件属性直接复制
wrappedProps[key] = val;
}
}

return wrappedProps;
};

const processVNode = (node) => {
// 跳过非VNode对象(文本节点、注释节点等)
if (!node || typeof node !== "object" || !node.type) {
return node;
}

const originalProps = node.props || {};

// 检查是否有事件属性需要处理
const hasEventProps = Object.keys(originalProps).some(key =>
key.startsWith("on") && typeof originalProps[key] === "function"
);

// 递归处理子节点
let processedChildren = node.children;
if (Array.isArray(node.children)) {
processedChildren = node.children.map((c) => processVNode(c));
}

if (!hasEventProps) {
// 没有事件属性,使用h函数重新创建VNode但保持原始props
return h(node.type, originalProps, processedChildren);
}

// 有事件属性,使用包装后的props重新创建VNode
const wrappedProps = wrapEventHandlers(originalProps);
return h(node.type, wrappedProps, processedChildren);
};

return children.map((c) => processVNode(c));
};
},
});