【涅槃】Vue3学习笔记(五)

470 阅读8分钟

Pinia在Vue3中的使用

Pinia 是 Vue 3 的官方推荐状态管理库,它继承了 Vuex 的设计理念,但提供了更简洁的 API 和更好的 TypeScript 支持。

Pinia 的核心概念

Store Pinia的核心是Store,它是一个独立的状态管理单元,用于存储状态(state)、派生状态(getters)和修改状态的方法(actions)。每个 Store 都是独立的,便于管理和复用。

State

state是 Store 中存储的数据,它是响应式的。在 Pinia 中,state是通过一个函数返回的,类似于 Vue 3 的setup()函数。

Getters

getters是基于state的派生状态,类似于 Vue 的计算属性。它可以通过computed来实现。

Actions

actions是用于修改state的方法,可以是同步的,也可以是异步的。

Pinia 的安装与配置

安装 Pinia

在 Vue 3 项目中,可以通过 npm 安装 Pinia:

npm install pinia

配置 Pinia

在项目的入口文件(如main.jsmain.ts)中引入并安装 Pinia:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount('#app');

定义 Store

src/stores文件夹中创建一个 Store 文件(如counter.js),并使用defineStore定义一个 Store:

// src/stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  // 定义状态
  state: () => ({
    count: 0
  }),

  // 定义派生状态
  getters: {
    doubleCount: (state) => state.count * 2
  },

  // 定义修改状态的方法
  actions: {
    increment() {
      this.count++;
    }
  }
});

在组件中使用 Store

使用useCounterStore

在组件中引入并使用 Store:

<template>
  <div>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">Increment</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter';

const counterStore = useCounterStore();
</script>

使用storeToRefs

为了保持响应式,可以使用storeToRefs解构 Store 的状态和 getters:

<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
</script>

异步 Actions

Pinia 支持异步操作,可以直接在actions中使用async

// src/stores/user.js
import { defineStore } from 'pinia';
import axios from 'axios';

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null
  }),

  actions: {
    async fetchUser(userId) {
      const response = await axios.get(`/api/users/${userId}`);
      this.user = response.data;
    }
  }
});

在组件中调用异步action

<script setup>
import { useUserStore } from '@/stores/user';

const userStore = useUserStore();

async function loadUser() {
  await userStore.fetchUser(1);
}
</script>

持久化存储

Pinia 提供了插件支持,可以将状态持久化到localStoragesessionStorage

安装持久化插件

npm install pinia-plugin-persistedstate

配置持久化

在 Store 中配置持久化:

// src/stores/counter.js
import { defineStore } from 'pinia';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),

  persist: {
    key: 'counter', // 持久化存储的 key
    storage: localStorage // 使用 localStorage
  }
});

调试

Pinia 支持 Vue Devtools,可以直接在 Devtools 中查看和调试 Store 的状态。

总结

Pinia 是 Vue 3 的新一代状态管理库,具有以下特点:

  • 简洁的 API:比 Vuex 更简洁,更易于上手。
  • 更好的 TypeScript 支持:提供了完整的类型推导。
  • 模块化设计:每个 Store 都是独立的,便于管理和复用。
  • 异步支持:支持异步操作,无需额外的库。
  • 持久化支持:通过插件支持状态持久化。

通过合理使用 Pinia,可以更好地管理 Vue 3 项目中的状态,提高开发效率和代码可维护性。

Vue3中的组件通信

在 Vue 3 中,组件之间的通信是构建复杂应用时的核心需求之一。Vue 提供了多种方式来实现组件之间的通信,包括父子组件通信、兄弟组件通信以及跨层级通信。

父子组件通信

父组件向子组件通信:通过props

props是子组件接收父组件数据的标准方式。父组件通过子组件的属性传递数据,子组件通过props定义接收的数据。

子组件:

<template>
  <div>
    <p>{{ title }}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: String
  }
};
</script>

父组件:

<template>
  <ChildComponent :title="headerTitle" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      headerTitle: 'Hello Vue 3!'
    };
  }
};
</script>

子组件向父组件通信:通过$emit

子组件可以通过 $emit 触发事件,父组件监听这些事件来接收数据。

子组件:

<template>
  <button @click="sendMessage">Send Message</button>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('message', 'Hello from child!');
    }
  }
};
</script>

父组件:

<template>
  <ChildComponent @message="handleMessage" />
  <p>{{ message }}</p>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      message: ''
    };
  },
  methods: {
    handleMessage(payload) {
      this.message = payload;
    }
  }
};
</script>

兄弟组件通信

通过父组件中转

兄弟组件之间不能直接通信,但可以通过父组件中转数据。

父组件:

<template>
  <ChildA :message="message" @update-message="updateMessage" />
  <ChildB :message="message" />
</template>

<script>
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';

export default {
  components: {
    ChildA,
    ChildB
  },
  data() {
    return {
      message: 'Hello!'
    };
  },
  methods: {
    updateMessage(newMessage) {
      this.message = newMessage;
    }
  }
};
</script>

ChildA:

<template>
  <button @click="sendMessage">Send Message</button>
</template>

<script>
export default {
  props: {
    message: String
  },
  methods: {
    sendMessage() {
      this.$emit('update-message', 'New message from ChildA');
    }
  }
};
</script>

ChildB:

<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  props: {
    message: String
  }
};
</script>

使用全局状态管理(如 Pinia)

兄弟组件可以通过全局状态管理工具(如 Pinia)共享状态,从而实现通信。

Pinia Store:

import { defineStore } from 'pinia';

export const useSharedStore = defineStore('shared', {
  state: () => ({
    sharedMessage: 'Hello from Pinia!'
  })
});

兄弟组件 A:

<template>
  <button @click="updateMessage">Update Message</button>
</template>

<script setup>
import { useSharedStore } from '@/stores/shared';

const store = useSharedStore();

function updateMessage() {
  store.sharedMessage = 'New message from ChildA';
}
</script>

兄弟组件 B:

<template>
  <p>{{ sharedMessage }}</p>
</template>

<script setup>
import { useSharedStore } from '@/stores/shared';

const store = useSharedStore();
const { sharedMessage } = store;
</script>

跨层级通信

使用事件总线(不推荐)

事件总线是一种全局事件触发器,但它的缺点是难以维护,容易导致代码混乱。Vue 3 中不推荐使用事件总线,建议使用全局状态管理工具(如 Pinia)

使用全局状态管理(推荐)

通过 Pinia 或 Vuex 管理全局状态,任何组件都可以访问和修改状态,从而实现跨层级通信。

Pinia Store:

import { defineStore } from 'pinia';

export const useGlobalStore = defineStore('global', {
  state: () => ({
    globalMessage: 'Hello from Global Store!'
  })
});

任意组件:

<template>
  <p>{{ globalMessage }}</p>
  <button @click="updateGlobalMessage">Update Global Message</button>
</template>

<script setup>
import { useGlobalStore } from '@/stores/global';

const store = useGlobalStore();
const { globalMessage } = store;

function updateGlobalMessage() {
  store.globalMessage = 'New global message!';
}
</script>

提供/注入(Provide/Inject)

使用场景

适用于祖先组件向后代组件(包括多层嵌套的组件)传递数据,但不推荐用于常规的父子组件通信。

使用方法 祖先组件:

<template>
  <ChildComponent />
</template>

<script>
import { provide, reactive } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  setup() {
    const state = reactive({
      message: 'Hello from Ancestor!'
    });

    provide('state', state);

    return {};
  }
};
</script>

后代组件:

<template>
  <p>{{ message }}</p>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    const state = inject('state');

    return {
      message: state.message
    };
  }
};
</script>

总结

在 Vue 3 中,组件之间的通信可以通过以下几种方式实现:

父子组件通信:

  • 父组件通过props向子组件传递数据。
  • 子组件通过$emit向父组件传递数据。

兄弟组件通信:

  • 通过父组件中转。
  • 使用全局状态管理工具(如 Pinia)。

跨层级通信:

  • 使用全局状态管理工具(推荐)。
  • 使用提供/注入(适用于特定场景)。

全局状态管理:

  • 使用 Pinia 或 Vuex 管理全局状态,适用于复杂应用。

选择哪种通信方式取决于你的具体需求和应用的复杂性。对于简单的父子组件通信,props$emit是最直接的方式;对于复杂的应用,全局状态管理工具(如 Pinia)是更好的选择。

vue3 <Teleport> 标签

什么是<Teleport>?

<Teleport>是 Vue 3 引入的一个内置组件,用于将组件的内容渲染到 DOM 树中的任意位置,而不受组件层级结构的限制。它的核心作用是将一个组件的子元素“传送”到另一个指定的 DOM 节点中,而不会改变组件在 Vue 组件树中的位置。

<Teleport>的基本用法 <Teleport>通过to属性指定目标容器,可以是一个 CSS 选择器字符串或一个 DOM 元素。

例如:

<template>
  <teleport to="body">
    <div class="modal">
      <p>这是一个模态框</p>
    </div>
  </teleport>
</template>

在这个例子中,<div class="modal">的内容会被渲染到<body>元素的末尾,而不是其直接的父组件内部。

<Teleport>的适用场景

模态框和弹出窗口 模态框和弹出窗口通常需要显示在页面的最顶层,不受其他 DOM 元素的影响。使用<Teleport>,可以轻松地将这些元素渲染到页面的最外层,确保它们总是显示在其他内容之上。

全局通知和提示

全局通知和提示通常需要显示在页面的顶部或特定位置,以便用户能够立即注意到。使用<Teleport>可以将通知组件渲染到一个固定的位置,而不受页面其他部分布局的影响。

解决z-index问题

在复杂的布局中,使用z-index来控制元素的堆叠顺序可能会变得复杂和混乱。<Teleport>提供了一种更优雅的方式来解决这类问题。

与第三方库集成

某些第三方库可能需要特定的 DOM 结构或位置来正确显示其内容。通过<Teleport>,可以将与这些库相关的组件渲染到合适的位置,以确保它们能够正常工作。

上下文菜单

上下文菜单(如右键菜单)需要根据鼠标位置动态显示。使用<Teleport>可以将菜单渲染到<body>或其他容器中,从而避免被其他元素遮挡。

<Teleport>的高级用法

动态目标

<Teleport>的目标容器可以通过动态绑定来改变。例如,可以根据屏幕大小将内容渲染到不同的位置:

<template>
  <teleport :to="target">
    <div class="content">
      动态传送的内容
    </div>
  </teleport>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const target = ref('body');

onMounted(() => {
  if (window.innerWidth < 768) {
    target.value = '#mobile-container';
  }
});
</script>

多个<Teleport>到同一目标

多个<Teleport>组件可以将其内容依次挂载到同一个目标元素上,按照先后顺序追加:

<template>
  <teleport to="#notifications">
    <div class="notification">通知 1</div>
  </teleport>

  <teleport to="#notifications">
    <div class="notification">通知 2</div>
  </teleport>
</template>

条件性传送

可以通过disabled属性控制<Teleport>是否生效:

<template>
  <teleport to="body" :disabled="isMobile">
    <div class="modal">
      <!-- 在移动端不会被传送,保持原位置 -->
    </div>
  </teleport>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const isMobile = ref(false);

onMounted(() => {
  isMobile.value = window.innerWidth < 768;
});
</script>

<Teleport>的注意事项

  • 目标容器必须存在:使用<Teleport>时,目标容器(如body或其他 DOM 元素)必须在页面中存在,否则<Teleport>无法正常工作。
  • 事件冒泡和捕获:由于<Teleport>改变了元素在 DOM 树中的实际位置,需要注意事件冒泡和捕获的行为。
  • 样式穿透:虽然<Teleport>改变了 DOM 结构,但组件的样式仍然可以通过:deep()选择器进行穿透。

总结

<Teleport>是 Vue 3 中一个非常强大的工具,特别适合处理那些需要脱离当前组件层级的 UI 元素。通过<Teleport>,你可以更灵活地控制组件的渲染位置,而不必担心 DOM 结构的限制。它在以下场景中特别有用:

  • 模态框和弹出窗口• 全局通知和提示 解决 z-index 问题
  • 与第三方库集成
  • 上下文菜单

总之,<Teleport>提供了一种优雅的方式来解决复杂的布局问题,同时保持代码的简洁和可维护性。

Vue3中的<Suspense>组件

<Suspense>是 Vue 3 引入的一个内置组件,用于处理异步组件和异步数据加载。它允许你在等待异步内容加载完成时显示一个备用内容(如加载指示器),从而提升用户体验。

<Suspense>的基本概念

<Suspense>是一个内置组件,用于处理异步组件或异步数据加载时的加载状态。它有两个主要作用:

  • 显示加载状态:在异步内容加载完成之前,显示一个备用内容(如加载指示器)。
  • 错误处理:捕获异步加载过程中可能发生的错误。
    • <Suspense>提供了两个插槽:
      • #default :用于放置异步组件或需要等待异步操作的内容。
      • #fallback :用于放置备用内容,在异步操作完成之前显示。

<Suspense>的基本用法

异步组件加载

<Suspense>常用于加载异步组件,例如通过defineAsyncComponent定义的组件。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
);
</script>

在这个例子中,AsyncComponent是一个异步加载的组件。在组件加载完成之前,<Suspense>会显示#fallback插槽中的内容(即“加载中...”)。

异步数据获取

<Suspense>也可以用于处理组件内部的异步数据加载。

<template>
  <Suspense>
    <template #default>
      <UserProfile :user="user" />
    </template>
    <template #fallback>
      <div class="loading">加载用户数据...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { ref } from 'vue';
import UserProfile from './UserProfile.vue';

async function fetchUserData() {
  const response = await fetch('/api/user');
  return await response.json();
}

const user = ref(await fetchUserData());
</script>

在这个例子中,fetchUserData是一个异步函数,用于获取用户数据。在数据加载完成之前,<Suspense>会显示#fallback插槽中的内容(即“加载用户数据...”)。

<Suspense>的高级用法

错误处理

<Suspense>可以捕获异步加载过程中发生的错误,并显示错误信息。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
    <template #error>
      <div class="error">加载失败,请稍后再试。</div>
    </template>
  </Suspense>
</template>

如果异步加载过程中发生错误,<Suspense>会显示#error插槽中的内容。 嵌套<Suspense>

<Suspense>可以嵌套使用,以支持更复杂的异步加载场景。

<template>
  <Suspense>
    <template #default>
      <div class="nested">
        <AsyncParent>
          <Suspense>
            <template #default>
              <AsyncChild />
            </template>
            <template #fallback>
              <div>加载子组件...</div>
            </template>
          </Suspense>
        </AsyncParent>
      </div>
    </template>
    <template #fallback>
      <div>加载父组件...</div>
    </template>
  </Suspense>
</template>

动态组件切换

<Suspense>可以与动态组件结合使用,支持异步组件的切换。

<template>
  <div class="tabs">
    <button
      v-for="(_, tab) in tabs"
      :key="tab"
      @click="currentTab = tab"
    >
      {{ tab }}
    </button>

    <Suspense>
      <template #default>
        <component :is="tabs[currentTab]" />
      </template>
      <template #fallback>
        <div class="loading">切换中...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue';

const currentTab = ref('tab1');
const tabs = {
  tab1: defineAsyncComponent(() => import('./tabs/Tab1.vue')),
  tab2: defineAsyncComponent(() => import('./tabs/Tab2.vue')),
  tab3: defineAsyncComponent(() => import('./tabs/Tab3.vue'))
};
</script>

<Suspense>的注意事项

避免无限循环:

  • 确保异步逻辑不会导致无限循环。
  • 示例:const data = await fetchData();而不是data.value = await fetchData();

合理的超时处理:

如果加载时间过长,可以显示超时提示。

示例:

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>
        <loading-spinner />
        <p v-if="timeout">加载时间过长,请检查网络连接。</p>
      </div>
    </template>
  </Suspense>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const timeout = ref(false);

onMounted(() => {
  setTimeout(() => {
    timeout.value = true;
  }, 5000);
});
</script>

资源清理:

在组件销毁时清理未完成的异步请求。

示例:

<script setup>
import { onUnmounted } from 'vue';

const controller = new AbortController();
const data = await fetch('/api/data', {
  signal: controller.signal
});

onUnmounted(() => {
  controller.abort(); // 取消未完成的请求
});
</script>

<Suspense>的最佳实践

将异步逻辑抽离到组合式函数:

示例:

export function useAsyncData(url) {
  return new Promise(async (resolve) => {
    const data = await fetch(url).then(r => r.json());
    resolve(data);
  });
}

提供更细粒度的加载状态:

示例:

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading-state">
        <loading-spinner />
        <loading-progress :progress="loadingProgress" />
        <p>{{ loadingMessage }}</p>
      </div>
    </template>
  </Suspense>
</template>

总结

<Suspense>是 Vue 3 中一个非常强大的工具,用于处理异步组件和异步数据加载时的加载状态和错误处理。它通过两个插槽(#default#fallback)提供了优雅的加载状态管理,并支持错误处理和嵌套使用。通过合理使用<Suspense>,可以显著提升用户体验,尤其是在处理复杂的异步加载场景时。