组件化概述
Vue 组件化是指将应用划分为独立的、可复用的模块,每个模块都拥有自己的模板、样式和逻辑,从而构建出灵活、易于维护和扩展的应用
在 Vue3 提供了一种新的代码组织和复用方式,引入了 Composition API 和 script setup 语法糖,可以将相关的逻辑代码拆分为独立的模块,在 setup 函数中引入并使用这些模块,组件可以抽象出这些复用的模块,方便扩展和维护。
Composition API 实践
自定义 Hook — 优化逻辑组织和复用
在 Vue2 中复用代码,一般是抽象到 Mixin 中,不过 Mixin 自身存在缺陷,如一个组件引入多个Mixin 会导致代码执行变得复杂和难以理解,命名容易重复,数据来源不清晰,调试和维护困难
为了解决这些问题,Vue3 引入了 Composition API ,将响应式、computed、watch、生命周期等封装成一个个独立的 Hook 函数,利于代码组织和复用
以拖拽功能为例,将拖拽相关的逻辑封装在一个独立的组合函数中,从业务中分离,内聚方法,在不同的模块复用拖拽的功能
import { reactive, toRefs } from 'vue';
export function useDraggable() {
// 拖拽数据
const state = reactive({
isDragging: false,
x: 0,
y: 0
});
// 拖拽开始,如记录当前位置,注册拖拽移动和拖拽结束事件
function startDrag(event) {
state.isDragging = true;
state.x = event.clientX;
state.y = event.clientY;
// 注册事件
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag)
}
// 拖拽中,计算移动距离
function handleDrag(event) {
if (state.isDragging) {
const dx = event.clientX - state.x;
const dy = event.clientY - state.y;
// 更新拖拽元素的位置
// ...
}
}
// 拖拽结束,释放资源,还原状态
function endDrag() {
state.isDragging = false;
// 销毁事件,释放资源
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', endDrag);
}
onMounted(() => {
// 其他初始化逻辑
});
onBeforeUnmount(() => {
// 清理工作
});
// 暴露属性和方法
return {
...toRefs(state),
startDrag,
endDrag,
handleDrag
};
}
useDraggable 将开始拖拽、拖拽中、拖拽结束的逻辑定义在一个函数中,在需要拖拽的元素上使用指令或绑定事件监听器
<template>
<div v-draggable="dragHandlers"></div>
</template>
<script setup>
import { useDraggable } from './useDraggable';
const dragHandlers = useDraggable();
</script>
生命周期钩子函数
Vue 3 的生命周期钩子函数在组件的不同阶段执行特定的操作,以便控制和管理组件的状态和数据。
1、数据获取和初始化:
在 setup 函数初始化或组件挂载后,获取数据并进行初始化操作
import { onMounted, reactive } from 'vue';
const data = reactive({
items: []
});
// 初始化渲染,执行数据获取逻辑
fetchItems().then(response => {
data.items = response.data;
});
onMounted(() => {
// 在DOM挂载后,执行数据获取逻辑
fetchItems2().then(response => {
data.items = response.data;
});
});
2、第三方库初始化
在组件挂载后,使用 onMounted 钩子函数来初始化第三方库或执行需要访问 DOM 的操作。
import { onMounted } from 'vue';
import { initChart } from 'chart-library';
onMounted(() => {
// 初始化图表库
initChart();
// 执行其他 DOM 操作
});
3、资源清理和取消异步任务
在组件卸载前,使用 onBeforeUnmount 钩子函数来清理资源或取消未完成的异步任务,及时释放资源,防止内存泄漏
import { onBeforeUnmount } from 'vue';
import { cleanupResources, cancelAsyncTask } from 'utils';
onBeforeUnmount(() => {
// 清理资源
cleanupResources();
// 取消异步任务
cancelAsyncTask();
});
watch和computed数据监测和计算
1、使用 watch 监听响应式数据变化并动态更新 class 和 style 属性:
<template>
<div :class="classNames" :style="dynamicStyle">
<!-- 内容 -->
</div>
</template>
<script setup>
import { watch, ref } from 'vue';
const isActive = ref(false);
const color = ref('red');
const classNames = ref('');
const dynamicStyle = ref({});
watch([isActive, color], ([newIsActive, newColor], [oldIsActive, oldColor]) => {
// 执行逻辑,更新 class 和 style 属性
updateClassNames();
updateDynamicStyle();
});
// 监听逻辑
function updateClassNames() {
// 根据 isActive 值更新 class
if (isActive.value) {
classNames.value = 'active';
} else {
classNames.value = '';
}
}
function updateDynamicStyle() {
// 根据 color 值更新动态的 style
dynamicStyle.value = {
backgroundColor: color.value
};
}
updateClassNames();
updateDynamicStyle();
</script>
2、使用 computed 计算派生的 class 和 style 属性:
<template>
<div :class="classNames" :style="dynamicStyle">
<!-- 内容 -->
</div>
</template>
<script setup>
import { computed, reactive } from 'vue';
const data = reactive({
isActive: false,
color: 'red'
});
const classNames = computed(() => {
// 根据 isActive 值计算 class
return data.isActive ? 'active' : '';
});
const dynamicStyle = computed(() => {
// 根据 color 值计算动态的 style
return {
backgroundColor: data.color
};
});
</script>
根据具体的业务需求和场景,灵活运用 watch 和 computed,
watch 的使用场景:
- 监听数据的变化并执行副作用操作,如发送网络请求、更新 DOM、触发动画效果等。
- 监听数据的变化并触发其他逻辑,如根据数据变化更新状态、触发路由导航等。
- 监听多个数据的变化并进行联动操作,如两个日期选择器之间的日期范围联动。
computed 的使用场景:
- 根据多个数据的组合计算动态样式,如根据用户喜好的主题和字体大小计算动态的样式对象。
- 根据数据的变化动态生成 HTML 或其他模板内容,如根据用户的输入生成动态的表格或列表。
使用注意事项:
- 避免过度使用 watch 和 computed:
过度使用它们可能导致代码复杂性和维护性的降低。在使用时要权衡利弊,避免不必要的计算和监听。
- 注意 computed 的计算开销:
computed 是惰性求值的,只有在其依赖的响应式数据变化时才会重新计算。然而,如果 computed 的计算逻辑复杂或依赖的数据较多,可能会引起性能问题。确保计算逻辑的复杂度和依赖关系的合理性。
组件通信
在 Vue3 组件通信,常用方案有
- 父子通信:props 和 Events
- 夸组件通信:provide 和 inject、透传属性($attrs)
- 应用通信:pinia 或 vuex
- 兄弟组件通信,可以使用 pinia 或 vuex,但过度使用 pinia 或 vuex 会造成业务逻辑变得复杂,数据管理混乱,
除了以上通信方式,在业务上使用 Vue3 api 和 模式封装一些通信方式,如事件总线、Hooks 状态函数,管理数据更方便,
事件总线通信实践
1、mitt 第三方库封装事件总线
总线文件
// EventBus.js
import mitt from 'mitt';
export const eventBus = mitt();
组件监听,接收信息
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { eventBus } from './EventBus';
const receivedMessage = ref('');
const handleMessage = (message) => {
receivedMessage.value = message;
};
// 监听接收信息
eventBus.on('messageSent', handleMessage);
onBeforeUnmount(() => {
// 组件销毁注销
eventBus.off('messageSent', handleMessage);
});
</script>
组件派发,传递信息
<template>
<div>
<p>{{ receivedMessage }}</p>
</div>
</template>
<script setup>
import { useEventBus } from './EventBus';
const eventBus = useEventBus();
function sendMessage() {
// 派发事件
eventBus.emit('messageSent', 'Hello from Component A!');
}
</script>
2、provide 和 inject 实现事件总线
- 根据发布订阅模式,封装事件总线,创建一个新的 eventBus.js 文件
// eventBus.js
import { provide, inject } from 'vue';
const EventBusSymbol = Symbol();
export function createEventBus() {
const listeners = {};
// 监听事件
function on(event, callback) {
if (!listeners[event]) {
listeners[event] = [];
}
listeners[event].push(callback);
}
// 注销事件
function off(event, callback) {
if (listeners[event]) {
const index = listeners[event].indexOf(callback);
if (index !== -1) {
listeners[event].splice(index, 1);
}
}
}
// 派发事件
function emit(event, ...args) {
if (listeners[event]) {
listeners[event].forEach(callback => {
callback(...args);
});
}
}
return {
on,
off,
emit
};
}
export function provideEventBus() {
const eventBus = createEventBus();
provide(EventBusSymbol, eventBus);
}
export function useEventBus() {
const eventBus = inject(EventBusSymbol);
if (!eventBus) {
throw new Error('EventBus is not provided.');
}
return eventBus;
}
2、在父组件中使用 provideEventBus 提供事件总线
// ParentComponent.vue
<template>
<div>
<ChildComponentA />
<ChildComponentB />
</div>
</template>
<script setup>
import { provideEventBus } from './eventBus';
import ChildComponentA from './ChildComponentA.vue';
import ChildComponentB from './ChildComponentB.vue';
provideEventBus();
</script>
3、在兄弟组件中使用 useEventBus 来获取事件总线并进行通信:
ChildComponentA.vue
// ChildComponentA.vue
<template>
<button @click="sendMessage">Send Message to Component B</button>
</template>
<script setup>
import { useEventBus } from './eventBus';
const eventBus = useEventBus();
function sendMessage() {
eventBus.emit('messageSent', 'Hello from Component A!');
}
</script>
ChildComponentB.vue
// ChildComponentB.vue
<template>
<div>
<p>{{ receivedMessage }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useEventBus } from './eventBus';
const eventBus = useEventBus();
const receivedMessage = ref('');
function handleMessage(message) {
receivedMessage.value = message;
}
onMounted(() => {
eventBus.on('messageSent', handleMessage);
});
onBeforeUnmount(() => {
eventBus.off('messageSent', handleMessage);
});
</script>
封装 Hooks 实现通信
封装 hook 实现应用通信,定义 createHooks 函数,创建一个 state 数据,本质上利用了 闭包 原理,维护一个私有变量,use 用于注册添加一个函数,返回一个函数用于取消注册,exec 派发执行函数
// 创建hook
function createHooks (isReactive) {
const state = isReactive ? reactive({
innerHandlers: []
}) : ({
innerHandlers: []
})
// 注册事件
const use = (handler) => {
state.innerHandlers = [...state.innerHandlers, handler]
return () => eject(handler)
}
// 注销事件
const eject = (handler) => {
state.innerHandlers = state.innerHandlers.filter(i => i !== handler)
}
// 派发执行事件
const exec = (arg, refresh) => {
if (state.innerHandlers.length === 0) {
return arg
}
let index = 0
const innerHandlers = [...state.innerHandlers]
let innerHandler = innerHandlers[index]
while (innerHandler) {
innerHandler(arg, refresh)
index++
innerHandler = innerHandlers[index]
}
return arg
}
return { use, eject, exec, state, getListeners: () => [...state.innerHandlers] }
}
// 创建钩子方法,统一管理
export function useVisualEditorHooks ({ state }) {
const hooks = {
// 拖拽开始动作
onDragstart: createHooks(),
// 拖拽结束动作
onDragend: createHooks(),
...
}
return hooks
}
createHooks 可以根据业务场景创建多个对象,然后利用该对象实现业务之间的通信,以编辑器组件开始拖拽为例
dragstart function({ component, event }) => {
...
// 派发事件
const exitStart = hooks.onDragstart.exec(component, event)
// 在需要地方执行注销释放资源
exitStart()
},
dragstart 开始拖拽,将组件信息传入 exec,在需要监听拖拽事件,进行接收数据,处理业务逻辑
hooks.onDragstart.use((data) => {
state.isDragging = true
...
})
动态组件高级用法
为了减少使用 if-else 使用动态组件 <component> , 通过 :is 动态渲染组件
1、使用key属性:
Vue 遵循就地复用策略,在某些情况下会复用,切换就出现异常,可以在动态组件切换时,为组件添加唯一的 key 属性,确保组件之间的状态得到正确地保留和更新,而不会出现冲突:
<template>
<div>
<component :is="currentComponent" :key="currentComponent"></component>
<button @click="toggleComponent">Toggle Component</button>
</div>
</template>
2、动态组件的异步加载
动态组件异步加载,可以拆离单独文件,提高应用程序的性能。使用 import 函数可以实现异步加载组件:
<template>
<div>
<component :is="currentComponent" :key="currentComponent" :message="message"></component>
<button @click="toggleComponent">Toggle Component</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const currentComponent = ref(null);
const toggleComponent = async () => {
const component = currentComponent.value === 'ComponentA' ? 'ComponentB' : 'ComponentA';
currentComponent.value = await import(`./components/${component}.vue`);
};
</script>
3、动态组件的缓存策略
动态组件在切换后会被销毁和重新创建,但在某些情况下,希望保留组件的状态和避免重新渲染,可以设置 keep-alive 属性来缓存动态组件
<keep-alive>
<component :is="currentComponent" :key="currentComponent" :message="message"></component>
</keep-alive>
封装一个 Vue 插件
一些公共的组件库、业务包,抽离一个独立的 npm 包,方便做维护和扩展,进行版本管理,需要利用 vue 提供的插件接口 use 进行安装到项目中
以封装一个编辑器插件为例
- 创建一个 EditorPlugin.js 文件,用于封装编辑器插件
// EditorPlugin.js
import { createApp, ref, defineComponent, onMounted, onBeforeUnmount } from 'vue';
export const EditorPlugin = {
// 在执行 app.use 会执行 install 方法,传入 app 应用实例
install(app) {
const editorContainer = ref(null);
let editorInstance = null;
// 创建编辑器组件
const editorComponent = defineComponent({
setup() {
onMounted(() => {
// 编辑器实例
editorInstance = createEditor(editorContainer.value);
});
onBeforeUnmount(() => {
// 组件销毁,释放编辑器资源
destroyEditor(editorInstance);
});
},
template: `
<div ref="editorContainer"></div>
`
});
// 全局注册组件
app.component('Editor', editorComponent);
}
};
// 编辑器实例函数
function createEditor(container) {
const editor = {
container,
// Editor initialization logic goes here
// ...
};
return editor;
}
// 编辑器销毁函数
function destroyEditor(editor) {
// Editor cleanup logic goes here
// ...
}
- 在 main.js 中使用插件:
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { EditorPlugin } from './EditorPlugin';
const app = createApp(App);
app.use(EditorPlugin);
app.mount('#app');
- 在需要使用编辑器的组件中使用 <Editor> 组件:
// SomeComponent.vue
<template>
<div>
<Editor />
</div>
</template>
<script>
export default {
components: {
Editor: 'Editor'
}
};
</script>
在上述示例中,通过 EditorPlugin 对编辑器插件进行封装。在 install 方法中,定义了一个 Editor 组件,该组件会在 onMounted 钩子中创建编辑器实例,并在 onBeforeUnmount 钩子中销毁编辑器。这样,在每个使用 <Editor> 组件的地方,都会自动创建和销毁编辑器实例,实现编辑器功能的复用。
扩展: Vue插件安装 use 函数在 vue 源码实现逻辑不多,挺清晰,以下是核心代码理解
// 简化版 use 函数
export function use(plugin: any, ...options: any[]): any {
// 判断是否已经安装过该插件,避免重复安装
if (plugin.__installed) {
return;
}
// 插件可以是一个对象或一个函数
if (isFunction(plugin)) {
// 如果是函数,则执行该函数,并将 app 作为第一个参数传入
plugin(app, ...options);
} else if (isFunction(plugin.install)) {
// 如果插件提供了 install 方法,则执行该方法,并将 app 作为第一个参数传入
plugin.install(app, ...options);
}
// 标记插件已安装
plugin.__installed = true;
}
纯 JS 编程组件
Vue template 模板编写带来了很大的便利,但也具有一定的局限性和灵活性,如果要编写复杂的交互的组件,模板上可能要加很多判断,扩展性和维护性不好,使用类似于 JSX 编程会有意想不到的效果
以创建一个任务列表组件为例:
const TaskList = {
setup () {
const tasks = ref([
{ id: 1, title: '完成Vue3学习', completed: false },
{ id: 2, title: '写一篇技术文章', completed: false },
{ id: 3, title: '学习新的前端框架', completed: false }
])
return () => {
return (
<ul>
{ tasks.value.map(item =>
<li>{ item.title }</li>
)}
</ul>
)
}
}
}
// 初始化组件生成vdom
const vm = createVNode(TaskList)
// 创建容器
const container = document.createElement('div')
// render通过patch 变成dom
render(vm, container)
document.body.appendChild(container.firstElementChild)
1、在 setup 函数中编写页面,返回一个函数作为渲染内容,使用 jsx 代替 h 函数创建节点
2、createVNode 渲染函数,将模板生成虚拟 DOM 节点
3、render 调用 patch 把虚拟 DOM 转化为真实 DOM
4、最后拿到 DOM 添加到页面上
组件双向数据绑定 v-model
v-model 除了在表单输入框可以用外,其实也可以应用在组件上,可以实现父组件和子组件之间的双向绑定
- 在自定义组件上使用 v-model,需要在组件中声明 modelValue 属性,并在需要更新父组件数据时,使用 emit 方法触发 update:modelValue 事件
<!-- CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
export default {
props: {
modelValue: String
}
};
</script>
父组件引入使用,v-model 由于是双向绑定,父组件就不需要使用事件监听来更改数据了
<!-- ParentComponent.vue -->
<template>
<div>
<h2>Parent Component</h2>
<CustomInput v-model="message" />
<p>Message from Child Component: {{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const message = ref('');
</script>
- v-model 如果不希望使用 value 作为参数,并且需要绑定多个参数
在自定义组件上使用 v-model:propName 来指定不同的 prop 名称,并使用 @update:propName 来指定不同的事件名称
// 自定义组件
<!-- CustomInput.vue -->
<template>
<div>
<input :value="name" :disabled="isDisabled" @input="updateValue" />
</div>
</template>
<script>
import { ref, defineProps, defineEmits } from 'vue';
// 使用 defineProps() 和 defineEmits() 定义 props 和 emit
const props = defineProps(['name', 'isDisabled']);
const emitUpdateValue = defineEmits(['update:name', 'update:isDisabled']);
// 创建一个响应式的 valueRef
const valueRef = ref(props.name);
// 在输入框值改变时触发自定义的 updateValue 事件
function updateValue(event) {
valueRef.value = event.target.value;
emitUpdateValue('update:name', valueRef.value);
}
</script>
在父组件引入使用
<template>
<CustomInput v-model:name="message" v-model:disabled="isDisabled" />
</template>
内置高级组件开发
keep-alive 缓存组件
keep-alive 组件用于缓存其他组件,避免它们在切换时被销毁和重新创建。这样可以提高应用的性能,特别是对于包含复杂数据计算或渲染逻辑的组件
由于过度使用缓存组件,会占有大量内存,所以提供 include 和 exclude 属性,用于指定需要缓存或排除的组件
使用 keep-alive 与 router-view 和动画效果相结合可以实现在切换路由时缓存组件,并为组件切换添加过渡效果
<router-view v-slot="{ Component }">
<transition name="fade">
<keep-alive :include="cachedComponents">
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
Teleport 传送组件
经常有遇到一个弹窗的场景,Vue3 提供了非常好用实现弹窗的组件 Teleport ,它可以将组件的内容渲染到任意的 DOM 节点上,从而实现在组件外部插入弹窗内容的效果
下面实现一个简单的弹窗组件示例,使用 Teleport 将 Dialog 组件的内容渲染到 body 上,这样它可以在页面的任意位置显示。
<!-- Dialog.vue -->
<template>
<teleport to="body">
<div class="dialog-overlay" v-if="show">
<div class="dialog-container">
<div class="dialog-header">
<h2>{{ title }}</h2>
<button @click="close">Close</button>
</div>
<div class="dialog-content">
<slot></slot>
</div>
</div>
</div>
</teleport>
</template>
<script>
import { ref, defineComponent } from 'vue';
export default defineComponent({
props: {
title: String,
},
setup(props) {
const show = ref(false);
const open = () => {
show.value = true;
};
const close = () => {
show.value = false;
};
return {
show,
open,
close,
};
},
});
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-container {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-content {
margin-top: 10px;
}
</style>
异步组件 defineAsyncComponent 和 Suspense的应用
异步组件和 Suspense 可以用于实现代码分割和懒加载,从而提高应用的性能。异步组件允许将组件的加载推迟到需要的时候,而 Suspense 则用于处理异步组件加载过程中的加载状态。
- 使用 defineAsyncComponent 函数来定义一个异步组件。这样可以将组件的加载推迟到需要的时候,从而减少初始加载时的包体积
// 使用 defineAsyncComponent 定义异步组件
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
export default AsyncComponent;
- 在父组件中使用 <Suspense> 组件来处理异步组件的加载状态。在异步组件加载完成之前,可以在 <Suspense> 中添加加载中的提示或占位内容。
<!-- App.vue -->
<template>
<div>
<Suspense>
<!-- 使用异步组件 -->
<AsyncComponent />
<!-- 在异步组件加载完成之前,可以在 Suspense 中添加加载中的提示 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { Suspense } from 'vue';
import AsyncComponent from './AsyncComponent.js';
</script>
- 预加载和错误组件
使用 defineAsyncComponent 函数的 loader 选项来预加载异步组件, loadingComponent 加载中的组件,errorComponent 处理异步组件加载的错误
const asyncComponent = defineAsyncComponent({
loader: () => import('./components/AsyncComponent.vue'),
loadingComponent: LoadingComponent, // 可选,加载过程中显示的组件
errorComponent: ErrorComponent, // 可选,加载出错时显示的组件
});
作者:网页建筑师
链接:https://juejin.cn/post/7260819638123233337
本文暂时没有评论,来添加一个吧(●'◡'●)