Vue 3 组合式 API 进阶实战

前言

💡 痛点: Composition API 和 Options API 怎么选?Composables 如何设计成可复用?Vue 3 的响应式系统原理是什么?TypeScript 和 Vue 3 如何完美结合?

🎯 解决方案: 从响应式原理→组合式函数→依赖注入→Pinia→TypeScript→性能优化→测试,系统掌握 Vue 3 Composition API。
#mermaid-svg-cLBRG56zb72K9TV3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cLBRG56zb72K9TV3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cLBRG56zb72K9TV3 .error-icon{fill:#552222;}#mermaid-svg-cLBRG56zb72K9TV3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cLBRG56zb72K9TV3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cLBRG56zb72K9TV3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .marker.cross{stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cLBRG56zb72K9TV3 p{margin:0;}#mermaid-svg-cLBRG56zb72K9TV3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label text{fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label span{color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster-label span p{background-color:transparent;}#mermaid-svg-cLBRG56zb72K9TV3 .label text,#mermaid-svg-cLBRG56zb72K9TV3 span{fill:#333;color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .node rect,#mermaid-svg-cLBRG56zb72K9TV3 .node circle,#mermaid-svg-cLBRG56zb72K9TV3 .node ellipse,#mermaid-svg-cLBRG56zb72K9TV3 .node polygon,#mermaid-svg-cLBRG56zb72K9TV3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .rough-node .label text,#mermaid-svg-cLBRG56zb72K9TV3 .node .label text,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label,#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cLBRG56zb72K9TV3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .rough-node .label,#mermaid-svg-cLBRG56zb72K9TV3 .node .label,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label,#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label{text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .node.clickable{cursor:pointer;}#mermaid-svg-cLBRG56zb72K9TV3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .arrowheadPath{fill:#333333;}#mermaid-svg-cLBRG56zb72K9TV3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cLBRG56zb72K9TV3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cLBRG56zb72K9TV3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster text{fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 .cluster span{color:#333;}#mermaid-svg-cLBRG56zb72K9TV3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cLBRG56zb72K9TV3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cLBRG56zb72K9TV3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape p,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cLBRG56zb72K9TV3 .icon-shape .label rect,#mermaid-svg-cLBRG56zb72K9TV3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cLBRG56zb72K9TV3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cLBRG56zb72K9TV3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cLBRG56zb72K9TV3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 状态管理
依赖注入
组合式函数
Vue 3 响应式系统
业务库
工具库
生命周期
onMounted
onUnmounted
onWatcher
ref

基础响应式
reactive

对象响应式
computed

计算属性
watch/watchEffect

监听器
useCounter

状态逻辑
useFetch

数据获取
useStorage

持久化
useAuth

认证
usePagination

分页
provide

提供
inject

注入
Pinia

全局状态
Plugin

插件

Vue 3 Composition API 核心优势:

优势 说明 对比 Options API
逻辑复用 Composable 函数提取逻辑 Mixins 有命名冲突/来源不明
更好的类型推导 TypeScript 完美支持 Options API 需复杂泛型
更灵活的代码组织 按逻辑而非选项分组 同一逻辑分散在 data/methods/computed
更小的生产包 Tree-shaking 支持 全部打包
更好的性能 更好的虚拟 DOM diff -

一、响应式系统原理

1.1 ref 与 reactive

typescript 复制代码
// ===== ref:基础类型响应式 =====

import { ref, computed, watch } from 'vue';

// ref 包装基础类型
const count = ref(0);

// .value 访问(模板中自动解包)
console.log(count.value);  // 0
count.value++;
console.log(count.value);  // 1

// ref 对象结构
const doubleCount = computed(() => count.value * 2);

// ref 也接受复杂对象
const timerRef = ref<number | null>(null);
timerRef.value = setInterval(() => { /* ... */ }, 1000);

// ===== reactive:对象响应式 =====

import { reactive, readonly, toRefs } from 'vue';

// reactive 只能用于对象/数组
const state = reactive({
  user: { name: '张三', age: 28 },
  posts: [],
  loading: false,
});

// 自动解包(模板中)
state.user.name = '李四';
state.loading = true;

// readonly:防止外部修改
const stateCopy = readonly(state);

// toRefs:将 reactive 对象转为一堆 ref
const { user, loading } = toRefs(state);
// → user 是 ref<{name, age}>
// → 修改 user.value 会影响原 state

// ===== 响应式陷阱 =====

// ❌ 错误:解构丢失响应式
const { name, age } = state;  // 失去响应式!

// ✅ 正确:使用 toRefs
const stateRefs = toRefs(state);
const { name, age } = stateRefs;

// ❌ 错误:reactive 替换整个对象
state = reactive({ new: 'data' });  // 丢失响应式

// ✅ 正确:逐个属性修改
Object.assign(state, { new: 'data' });

// ❌ 错误:数组索引直接替换
state.posts[0] = newPost;  // 不触发更新

// ✅ 正确:使用 splice 或替换整个数组
state.posts.splice(0, 1, newPost);
// 或
state.posts = [...state.posts.slice(0, i), newPost, ...state.posts.slice(i + 1)];

1.2 响应式原理深度解析

typescript 复制代码
// ===== Vue 3 响应式原理 =====

// Vue 3 使用 Proxy 实现响应式
// 相比 Vue 2 的 Object.defineProperty:
//   ✅ 支持数组下标响应式
//   ✅ 支持新增属性响应式(不用 Vue.set)
//   ✅ 支持 Map/Set/WeakMap/WeakSet

import { reactive, effect, computed } from 'vue';

// 手写简化版响应式系统
function myReactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      
      // 依赖收集
      if (activeEffect) {
        // 建立 key → effect 的映射
        depsMap.get(key)?.add(activeEffect)
          ?? depsMap.set(key, new Set([activeEffect])).get(key);
      }
      
      return typeof result === 'object' && result !== null
        ? myReactive(result)  // 深度代理
        : result;
    },
    
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      const result = Reflect.set(target, key, value, receiver);
      
      if (oldValue !== value) {
        // 触发更新
        trigger(target, key);
      }
      
      return result;
    },
    
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      
      if (hadKey && result) {
        trigger(target, key);
      }
      
      return result;
    },
  });
}

// ===== watch vs watchEffect =====

import { watch, watchEffect, ref } from 'vue';

const userId = ref(1);
const userData = ref(null);

// watchEffect:立即执行,依赖自动追踪
// 当 userData.value 变化时自动重新执行
watchEffect(async () => {
  userData.value = await fetchUser(userId.value);
  console.log('fetched for:', userId.value);
});

// watch:惰性执行,精确控制
// 当 userId.value 变化时才执行
watch(userId, async (newId, oldId) => {
  userData.value = await fetchUser(newId);
  console.log(`user changed: ${oldId} → ${newId}`);
});

// 深度监听对象
watch(userData, (newData) => {
  console.log('user changed:', newData);
}, { deep: true });

// 监听多个源
watch([userId, userData], ([newId, newData], [oldId, oldData]) => {
  console.log('changed:', newId, newData);
});

// ===== computed 原理 =====

function myComputed<T>(getter: () => T) {
  let value: T;
  let dirty = true;  // 脏标记
  
  const effectFn = () => { 
    dirty = true;  // 依赖变化时标记为脏
  };
  
  // 追踪依赖
  watchEffect(effectFn);
  
  return {
    get value() {
      if (dirty) {
        value = getter();
        dirty = false;
      }
      return value;
    },
  };
}

二、组合式函数(Composables)

2.1 基础 Composable 设计

typescript 复制代码
// ===== useCounter 计数器 =====

// composables/useCounter.ts
import { ref, computed } from 'vue';

export function useCounter(options: {
  min?: number;
  max?: number;
} = {}) {
  const count = ref(0);
  const min = options.min ?? -Infinity;
  const max = options.max ?? Infinity;

  const canIncrement = computed(() => count.value < max);
  const canDecrement = computed(() => count.value > min);

  function increment(delta = 1) {
    if (canIncrement.value) {
      count.value = Math.min(count.value + delta, max);
    }
  }

  function decrement(delta = 1) {
    if (canDecrement.value) {
      count.value = Math.max(count.value - delta, min);
    }
  }

  function reset() {
    count.value = 0;
  }

  // 返回响应式状态和操作方法
  return {
    count,         // Ref<number>
    canIncrement,
    canDecrement,
    increment,
    decrement,
    reset,
  };
}

// ===== useLocalStorage 持久化 =====

// composables/useLocalStorage.ts
import { ref, watch, onMounted } from 'vue';

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
) {
  // 初始化:优先从 localStorage 读取
  const stored = localStorage.getItem(key);
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue);

  // 监听变化,自动持久化
  watch(
    data,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key);
      } else {
        localStorage.setItem(key, JSON.stringify(newValue));
      }
    },
    { deep: true }
  );

  // SSR 兼容
  onMounted(() => {
    const stored = localStorage.getItem(key);
    if (stored) {
      data.value = JSON.parse(stored);
    }
  });

  return data;
}

// 使用
const theme = useLocalStorage('theme', 'light');
// theme.value 自动持久化到 localStorage

// ===== useDebounceFn 防抖 =====

// composables/useDebounceFn.ts
import { ref, onUnmounted } from 'vue';

export function useDebounceFn<T extends (...args: any[]) => any>(
  fn: T,
  delay = 300
) {
  let timer: ReturnType<typeof setTimeout> | null = null;
  const pending = ref(false);

  function debouncedFn(...args: Parameters<T>) {
    pending.value = true;
    
    if (timer) clearTimeout(timer);
    
    timer = setTimeout(() => {
      fn(...args);
      pending.value = false;
      timer = null;
    }, delay);
  }

  // 组件卸载时清理
  onUnmounted(() => {
    if (timer) clearTimeout(timer);
  });

  return {
    fn: debouncedFn,
    pending,
    cancel: () => {
      if (timer) {
        clearTimeout(timer);
        timer = null;
        pending.value = false;
      }
    },
  };
}

// 使用
const { fn: searchUsers, pending } = useDebounceFn(
  async (query: string) => {
    users.value = await api.search(query);
  },
  300
);

2.2 业务级 Composable

typescript 复制代码
// ===== usePagination 分页 =====

// composables/usePagination.ts
import { ref, computed, watch } from 'vue';

export interface PaginationOptions<T> {
  /** 数据源 */
  data: Ref<T[]>;
  /** 每页条数 */
  pageSize?: number;
  /** 初始页码 */
  initialPage?: number;
}

export interface PaginationReturn<T> {
  /** 当前页数据 */
  currentItems: ComputedRef<T[]>;
  /** 总条数 */
  total: ComputedRef<number>;
  /** 总页数 */
  totalPages: ComputedRef<number>;
  /** 当前页码 */
  currentPage: Ref<number>;
  /** 每页条数 */
  pageSize: Ref<number>;
  /** 是否第一页 */
  isFirst: ComputedRef<boolean>;
  /** 是否最后一页 */
  isLast: ComputedRef<boolean>;
  /** 页码列表 */
  pageNumbers: ComputedRef<number[]>;
  /** 操作方法 */
  nextPage: () => void;
  prevPage: () => void;
  goToPage: (page: number) => void;
  reset: () => void;
}

export function usePagination<T>(
  options: PaginationOptions<T>
): PaginationReturn<T> {
  const { data, pageSize: initPageSize = 10, initialPage = 1 } = options;
  
  const currentPage = ref(initialPage);
  const pageSize = ref(initPageSize);

  const total = computed(() => data.value.length);
  const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
  
  const currentItems = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value;
    return data.value.slice(start, start + pageSize.value);
  });

  const isFirst = computed(() => currentPage.value === 1);
  const isLast = computed(() => currentPage.value === totalPages.value);

  const pageNumbers = computed(() => {
    const pages: number[] = [];
    const max = 7;  // 显示的页码数量
    let start = Math.max(1, currentPage.value - Math.floor(max / 2));
    let end = Math.min(totalPages.value, start + max - 1);
    
    if (end - start + 1 < max) {
      start = Math.max(1, end - max + 1);
    }
    
    for (let i = start; i <= end; i++) {
      pages.push(i);
    }
    return pages;
  });

  function nextPage() {
    if (!isLast.value) currentPage.value++;
  }

  function prevPage() {
    if (!isFirst.value) currentPage.value--;
  }

  function goToPage(page: number) {
    currentPage.value = Math.max(1, Math.min(page, totalPages.value));
  }

  function reset() {
    currentPage.value = 1;
  }

  // 数据源变化时重置
  watch(data, reset);

  return {
    currentItems,
    total,
    totalPages,
    currentPage,
    pageSize,
    isFirst,
    isLast,
    pageNumbers,
    nextPage,
    prevPage,
    goToPage,
    reset,
  };
}

// 使用
const allPosts = ref<Post[]>([]);
const {
  currentItems: displayedPosts,
  total,
  currentPage,
  pageNumbers,
  nextPage,
  prevPage,
  goToPage,
} = usePagination({ data: allPosts, pageSize: 20 });

2.3 useFetch 数据获取

typescript 复制代码
// ===== useFetch 封装 =====

// composables/useFetch.ts
import { ref, watchEffect, isRef, type Ref, type ComputedRef } from 'vue';

export interface UseFetchOptions {
  /** 立即请求 */
  immediate?: boolean;
  /** 刷新依赖 */
  refreshDeps?: Array<Ref | ComputedRef>;
  /** 请求配置 */
  credentials?: RequestCredentials;
  /** 超时时间(ms)*/
  timeout?: number;
}

export interface UseFetchReturn<T> {
  data: Ref<T | null>;
  error: Ref<Error | null>;
  isLoading: Ref<boolean>;
  isFetching: Ref<boolean>;
  isSuccess: Ref<boolean>;
  execute: (url?: string) => Promise<void>;
  refresh: () => Promise<void>;
  abort: () => void;
}

export function useFetch<T>(
  url: string | Ref<string>,
  options: UseFetchOptions = {}
): UseFetchReturn<T> {
  const {
    immediate = true,
    refreshDeps = [],
    credentials = 'same-origin',
    timeout = 30000,
  } = options;

  const data = ref<T | null>(null) as Ref<T | null>;
  const error = ref<Error | null>(null);
  const isLoading = ref(false);
  const isFetching = ref(false);
  const isSuccess = ref(false);

  let controller: AbortController | null = null;
  let timeoutId: ReturnType<typeof setTimeout> | null = null;

  async function execute(urlToFetch?: string) {
    const currentUrl = urlToFetch ?? (isRef(url) ? url.value : url);
    
    if (!currentUrl) return;

    // 取消之前的请求
    abort();

    controller = new AbortController();
    isLoading.value = true;
    isFetching.value = true;
    error.value = null;
    isSuccess.value = false;

    try {
      const response = await Promise.race([
        fetch(currentUrl, {
          method: 'GET',
          headers: { 'Content-Type': 'application/json' },
          credentials,
          signal: controller.signal,
        }),
        new Promise<never>((_, reject) => {
          timeoutId = setTimeout(() => reject(new Error('请求超时')), timeout);
        }),
      ]);

      clearTimeout(timeoutId!);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      data.value = await response.json();
      isSuccess.value = true;
    } catch (e: any) {
      if (e.name !== 'AbortError') {
        error.value = e;
        data.value = null;
      }
    } finally {
      isLoading.value = false;
      isFetching.value = false;
    }
  }

  function abort() {
    controller?.abort();
    controller = null;
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  }

  async function refresh() {
    await execute();
  }

  if (immediate) {
    if (refreshDeps.length > 0) {
      // 依赖变化时自动刷新
      watchEffect(() => {
        // 收集依赖
        refreshDeps.forEach(dep => {
          if (isRef(dep)) dep.value;
        });
        execute();
      });
    } else {
      execute();
    }
  }

  return {
    data,
    error,
    isLoading,
    isFetching,
    isSuccess,
    execute,
    refresh,
    abort,
  };
}

// ===== POST 请求封装 =====

export function usePost<T, R>(
  url: string | Ref<string>,
  body: Ref<T> | (() => T)
) {
  const result = ref<R | null>(null);
  const error = ref<Error | null>(null);
  const isLoading = ref(false);

  async function execute() {
    isLoading.value = true;
    error.value = null;
    
    try {
      const payload = typeof body === 'function' ? body() : body.value;
      const response = await fetch(isRef(url) ? url.value : url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      result.value = await response.json();
    } catch (e: any) {
      error.value = e;
    } finally {
      isLoading.value = false;
    }
  }

  return { result, error, isLoading, execute };
}

// ===== 使用示例 =====

// 基础 GET
const { data: user, isLoading, refresh } = useFetch<User>('/api/user/1');

// 带依赖刷新(userId 变化时自动重新请求)
const userId = ref(1);
const { data: userProfile } = useFetch<User>(
  computed(() => `/api/user/${userId.value}/profile`),
  { refreshDeps: [userId] }
);

// POST 请求
const postData = ref({ name: '', email: '' });
const { result, isLoading: posting, execute: submit } = usePost('/api/users', postData);

三、依赖注入

3.1 provide/inject

typescript 复制代码
// ===== 基础依赖注入 =====

// Parent.vue(提供方)
import { provide, ref } from 'vue';

export default {
  setup() {
    const theme = ref('light');
    const currentUser = ref({ id: 1, name: '张三' });

    // provide(key, value)
    provide('theme', theme);                    // 响应式
    provide('theme-mode', 'dark');             // 静态值
    provide('currentUser', currentUser);       // 响应式对象
    provide('api', new ApiService());          // 类实例
    
    // provide 也支持 symbol 作为 key
    const USER_KEY = Symbol('user');
    provide(USER_KEY, currentUser);
    
    // 更新响应式值,子组件自动同步
    function setTheme(newTheme: string) {
      theme.value = newTheme;
    }
    
    return { setTheme };
  },
};

// Child.vue(注入方)
import { inject, computed } from 'vue';

export default {
  setup() {
    // 注入
    const theme = inject<Ref<string>>('theme');           // 响应式
    const themeMode = inject<string>('theme-mode');      // 静态值
    const user = inject('currentUser') as Ref<User>;     // 断言类型
    
    // 注入带默认值
    const config = inject('appConfig', { apiUrl: '/api' });
    
    // 注入 symbol key
    const userBySymbol = inject(Symbol('user'));
    
    // 计算属性使用注入值
    const isDark = computed(() => theme?.value === 'dark');
    
    // inject 也支持 reactive 解构
    const { theme: injectedTheme } = inject<{
      theme: Ref<string>;
      user: Ref<User>;
    }>('app-context', {} as any);
    
    return { theme, themeMode, user, isDark };
  },
};

3.2 依赖注入进阶模式

typescript 复制代码
// ===== Context Provider 封装 =====

// composables/createContext.ts
import { InjectionKey, provide, inject } from 'vue';

export interface AuthContext {
  user: Ref<User | null>;
  isAuthenticated: ComputedRef<boolean>;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => Promise<void>;
}

export const AuthContextKey: InjectionKey<AuthContext> = Symbol('auth');

// 提供 Context
export function useAuthProvider() {
  const user = ref<User | null>(null);
  const isAuthenticated = computed(() => !!user.value);
  
  async function login(credentials: LoginCredentials) {
    user.value = await authApi.login(credentials);
  }
  
  async function logout() {
    await authApi.logout();
    user.value = null;
  }
  
  const context: AuthContext = {
    user,
    isAuthenticated,
    login,
    logout,
  };
  
  provide(AuthContextKey, context);
  return context;
}

// 使用 Context(子组件)
export function useAuth() {
  const context = inject(AuthContextKey);
  if (!context) {
    throw new Error('useAuth() 必须在 AuthProvider 内调用');
  }
  return context;
}

// ===== 带泛型的 Context =====

// 通用 Context 创建工具
export function createContext<T>(key: InjectionKey<T>) {
  const provideFn = (value: T) => provide(key, value);
  const useFn = () => {
    const context = inject(key);
    if (!context) {
      throw new Error(`Context ${key.toString()} 未找到`);
    }
    return context;
  };
  return { provide: provideFn, use: useFn };
}

// 使用
const { provide: provideUser, use: useUser } = createContext<UserContext>(Symbol('user'));

// ===== 多层注入与覆盖 =====

// 提供方
provide('app-version', '2.0');

// 子组件覆盖
provide('app-version', '2.1');  // 会覆盖父级的值

// 孙组件获取
const version = inject('app-version');  // 拿到 2.1

四、Pinia 状态管理

4.1 Store 定义

typescript 复制代码
// ===== 定义 Store =====

// stores/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User } from '@/types';

export const useUserStore = defineStore('user', () => {
  // 状态
  const user = ref<User | null>(null);
  const token = ref<string | null>(localStorage.getItem('token'));
  const isLoading = ref(false);

  // 计算属性
  const isAuthenticated = computed(() => !!token.value && !!user.value);
  const userName = computed(() => user.value?.name ?? 'Guest');
  const userAvatar = computed(() => user.value?.avatar ?? '/default-avatar.png');

  // Actions
  async function login(credentials: { email: string; password: string }) {
    isLoading.value = true;
    try {
      const response = await api.post<{ user: User; token: string }>(
        '/auth/login',
        credentials
      );
      user.value = response.user;
      token.value = response.token;
      localStorage.setItem('token', response.token);
      return response;
    } finally {
      isLoading.value = false;
    }
  }

  async function logout() {
    await api.post('/auth/logout');
    user.value = null;
    token.value = null;
    localStorage.removeItem('token');
  }

  async function fetchUser() {
    if (!token.value) return;
    isLoading.value = true;
    try {
      user.value = await api.get<User>('/user/profile');
    } finally {
      isLoading.value = false;
    }
  }

  function updateProfile(data: Partial<User>) {
    if (user.value) {
      user.value = { ...user.value, ...data };
    }
  }

  return {
    // 状态
    user,
    token,
    isLoading,
    // 计算属性
    isAuthenticated,
    userName,
    userAvatar,
    // Actions
    login,
    logout,
    fetchUser,
    updateProfile,
  };
});

// ===== 跨 Store 调用 =====

import { useAuthStore } from './auth';
import { useCartStore } from './cart';

export const useOrderStore = defineStore('order', () => {
  const authStore = useAuthStore();  // 调用其他 store
  const cartStore = useCartStore();

  async function createOrder() {
    if (!authStore.isAuthenticated) {
      throw new Error('请先登录');
    }
    
    const items = cartStore.items;
    // 创建订单逻辑
    const order = await api.post('/orders', {
      userId: authStore.user!.id,
      items,
    });
    
    cartStore.clear();
    return order;
  }

  return { createOrder };
});

4.2 Pinia 插件与持久化

typescript 复制代码
// ===== Pinia 插件:状态持久化 =====

// plugins/pinia-persist.ts
import { defineStore } from 'pinia';
import type { PiniaPluginContext } from 'pinia';

interface PersistOptions {
  key?: string;
  storage?: Storage;
  paths?: string[];  // 只持久化这些路径
}

function createPersistPlugin(options: PersistOptions = {}) {
  const { key = 'pinia', storage = localStorage, paths } = options;

  return ({ store, options: storeOptions }: PiniaPluginContext) => {
    // 检查 store 是否配置了 persist
    const persist = (storeOptions as any).persist as PersistOptions | undefined;
    if (!persist) return;

    const persistKey = persist.key ?? `${key}-${store.$id}`;
    const persistPaths = persist.paths ?? paths;

    // 恢复状态
    const stored = storage.getItem(persistKey);
    if (stored) {
      const parsed = JSON.parse(stored);
      if (persistPaths) {
        persistPaths.forEach((p) => {
          (store.$state as any)[p] = parsed[p];
        });
      } else {
        store.$patch(parsed);
      }
    }

    // 监听变化,持久化
    store.$subscribe(
      (_, state) => {
        const toStore = persistPaths
          ? Object.fromEntries(persistPaths.map(p => [p, (state as any)[p]]))
          : state;
        storage.setItem(persistKey, JSON.stringify(toStore));
      },
      { detached: true }
    );
  };
}

// 注册插件
// main.ts
import { createPinia } from 'pinia';
const pinia = createPinia();
pinia.use(createPersistPlugin());
app.use(pinia);

// ===== Store 使用 persist =====

export const useSettingsStore = defineStore('settings', () => {
  const theme = ref<'light' | 'dark'>('light');
  const language = ref('zh-CN');
  const compactMode = ref(false);

  return { theme, language, compactMode };
}, {
  // 开启持久化
  persist: {
    key: 'app-settings',
    paths: ['theme', 'language'],  // 只持久化 theme 和 language
  },
});

4.3 Pinia 与 TypeScript

typescript 复制代码
// ===== 类型安全的 Store =====

// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

interface CounterState {
  count: number;
  lastUpdated: Date | null;
}

export const useCounterStore = defineStore('counter', () => {
  // 状态 + 类型
  const count = ref<number>(0);
  const lastUpdated = ref<Date | null>(null);

  // Getters(计算属性)
  const doubleCount = computed(() => count.value * 2);
  const isPositive = computed(() => count.value > 0);

  // Actions
  function increment(delta = 1) {
    count.value += delta;
    lastUpdated.value = new Date();
  }

  function decrement(delta = 1) {
    count.value -= delta;
    lastUpdated.value = new Date();
  }

  function reset() {
    count.value = 0;
    lastUpdated.value = null;
  }

  // $subscribe 返回状态快照的类型
  function getState(): CounterState {
    return {
      count: count.value,
      lastUpdated: lastUpdated.value,
    };
  }

  return {
    count,
    lastUpdated,
    doubleCount,
    isPositive,
    increment,
    decrement,
    reset,
    getState,
  };
});

// ===== 在组件中使用(类型自动推断)=====

const store = useCounterStore();
// count: Ref<number>
// doubleCount: ComputedRef<number>
// increment: (delta?: number) => void
store.increment(5);
store.count;  // number(类型安全)

五、生命周期与副作用

5.1 生命周期钩子

typescript 复制代码
// ===== 生命周期钩子对照表 =====

/*
Vue 3 Composition API          Vue 2 Options API
───────────────────────────────────────────────
setup()                         beforeCreate, created
onMounted()                     mounted
onUpdated()                     updated
onUnmounted()                   unmounted
onBeforeMount()                  beforeMount
onBeforeUpdate()                beforeUpdate
onBeforeUnmount()               beforeUnmount
onErrorCaptured()               errorCaptured
onRenderTracked()               renderTracked(DevTools)
onRenderTriggered()             renderTriggered(DevTools)
onServerPrefetch()              serverPrefetch(SSR)
*/

// ===== 完整示例 =====

import {
  onMounted,
  onUnmounted,
  onBeforeMount,
  onUpdated,
  onBeforeUpdate,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered,
} from 'vue';

export default {
  setup() {
    // 组件实例已创建,DOM 未挂载
    onBeforeMount(() => {
      console.log('onBeforeMount: DOM 即将挂载');
    });

    // DOM 已挂载
    onMounted(() => {
      console.log('onMounted: DOM 已挂载');
      
      // 绑定事件(最佳位置)
      window.addEventListener('resize', handleResize);
      
      // 初始化第三方库
      const chart = new Chart(canvasRef.value);
    });

    // DOM 更新前
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate: DOM 即将更新');
    });

    // DOM 更新后
    onUpdated(() => {
      console.log('onUpdated: DOM 已更新');
    });

    // 组件卸载前(清理)
    onUnmounted(() => {
      console.log('onUnmounted: 组件即将卸载');
      
      // 清理事件监听
      window.removeEventListener('resize', handleResize);
      
      // 销毁第三方实例
      chart?.destroy();
      
      // 清除定时器
      clearInterval(timerId);
    });

    // 错误捕获
    onErrorCaptured((err, instance, info) => {
      console.error('Error:', err);
      console.error('Component:', instance);
      console.error('Info:', info);
      
      // 返回 false 阻止错误传播
      return false;
    });

    // 调试钩子
    onRenderTracked(({ key, target, type }) => {
      console.log('render tracked:', key, target, type);
    });

    onRenderTriggered(({ key, target, type }) => {
      console.log('render triggered:', key, target, type);
    });

    return {};
  },
};

// ===== 定时器 Composable(自动清理)=====

// composables/useInterval.ts
import { ref, onUnmounted } from 'vue';

export function useInterval(callback: () => void, interval: number) {
  const isActive = ref(false);
  let timerId: ReturnType<typeof setInterval> | null = null;

  function start() {
    if (!isActive.value) {
      isActive.value = true;
      timerId = setInterval(callback, interval);
    }
  }

  function stop() {
    if (isActive.value) {
      isActive.value = false;
      if (timerId) {
        clearInterval(timerId);
        timerId = null;
      }
    }
  }

  // 组件卸载时自动停止
  onUnmounted(stop);

  return {
    isActive,
    start,
    stop,
    restart: () => { stop(); start(); },
  };
}

六、性能优化

6.1 虚拟滚动

typescript 复制代码
// ===== 虚拟滚动列表 =====

// composables/useVirtualScroll.ts
import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue';

export interface VirtualScrollOptions {
  /** 数据源 */
  items: Ref<any[]>;
  /** 每项高度 */
  itemHeight: number;
  /** 可视区域高度 */
  containerHeight: number;
  /** 缓冲区数量 */
  buffer?: number;
}

export function useVirtualScroll(options: VirtualScrollOptions) {
  const { items, itemHeight, containerHeight, buffer = 3 } = options;

  const scrollTop = ref(0);
  const containerRef = ref<HTMLElement>();

  // 可视区域能显示的项数
  const visibleCount = Math.ceil(containerHeight / itemHeight);

  // 起始索引
  const startIndex = computed(() => {
    return Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer);
  });

  // 结束索引
  const endIndex = computed(() => {
    return Math.min(
      items.value.length - 1,
      startIndex.value + visibleCount + buffer * 2
    );
  });

  // 可见项
  const visibleItems = computed(() => {
    return items.value.slice(startIndex.value, endIndex.value + 1);
  });

  // 偏移量(用于定位起始项)
  const offsetTop = computed(() => startIndex.value * itemHeight);

  // 总高度(撑开滚动区域)
  const totalHeight = computed(() => items.value.length * itemHeight);

  function onScroll(e: Event) {
    const target = e.target as HTMLElement;
    scrollTop.value = target.scrollTop;
  }

  // 滚动到指定索引
  function scrollToIndex(index: number) {
    if (containerRef.value) {
      containerRef.value.scrollTop = index * itemHeight;
    }
  }

  return {
    containerRef,
    visibleItems,
    offsetTop,
    totalHeight,
    startIndex,
    onScroll,
    scrollToIndex,
  };
}

// ===== 使用虚拟滚动 =====

const allUsers = ref<User[]>([]);

const {
  containerRef,
  visibleItems,
  offsetTop,
  totalHeight,
  scrollToIndex,
} = useVirtualScroll({
  items: allUsers,
  itemHeight: 60,
  containerHeight: 500,
});

// 加载 10000 条数据测试
onMounted(async () => {
  allUsers.value = await fetchUsers(10000);
});

6.2 懒加载与异步组件

vue 复制代码
<!-- ===== Suspense 异步组件 ===== -->

<template>
  <Suspense>
    <!-- 主要内容(异步)-->
    <template #default>
      <AsyncUserProfile :user-id="userId" />
    </template>
    
    <!-- 加载中 -->
    <template #fallback>
      <div class="loading-skeleton">
        <div class="skeleton-avatar"></div>
        <div class="skeleton-text"></div>
      </div>
    </template>
  </Suspense>
</template>

<!-- ===== 异步组件定义 ===== -->

<script setup lang="ts">
import { defineAsyncComponent, onMounted } from 'vue';

const AsyncUserProfile = defineAsyncComponent({
  loader: () => import('./UserProfile.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorBoundary,
  delay: 200,           // 延迟显示 loading
  timeout: 3000,        // 超时显示 error
  suspensible: false,   // 是否受 Suspense 控制
});
</script>

<!-- ===== 路由懒加载 ===== -->

<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue'),  // 懒加载
  },
  {
    path: '/users',
    component: () => import(/* webpackChunkName: "users" */ './views/Users.vue'),
  },
  {
    path: '/settings',
    component: () => import(
      /* webpackChunkName: "settings" */
      /* webpackPrefetch: true */
      './views/Settings.vue'
    ),
  },
];
</script>

七、Vue 3 + TypeScript 最佳实践

7.1 类型定义

typescript 复制代码
// ===== Vue 3 TypeScript 类型 =====

import type { Ref, ComputedRef } from 'vue';

// Props 定义
interface Props {
  title: string;
  count?: number;
  items: string[];
  onClick?: (id: number) => void;
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [],
});

// Emits 定义
const emit = defineEmits<{
  (e: 'update', value: string): void;
  (e: 'delete', id: number): void;
  (e: 'click', event: MouseEvent): void;
}>();

// 模板中调用
emit('update', 'new value');

// Expose 给父组件
defineExpose({
  open: () => void,
  close: () => void,
  value: props.title,
});

// Refs
const canvasRef = ref<HTMLCanvasElement>();
const inputRef = ref<InstanceType<typeof import('element-plus').ElInput>>();

// ===== 泛型组件 =====

// GenericList.vue
<script setup lang="ts" generic="T extends { id: string | number }">
import { computed } from 'vue';

const props = defineProps<{
  items: T[];
  renderItem: (item: T) => any;
}>();

const renderedItems = computed(() => props.items.map(props.renderItem));
</script>

<!-- 使用 -->
<GenericList :items="users" :render-item="(u) => u.name" />
<GenericList :items="products" :render-item="(p) => p.title" />

7.2 Vite 配置

typescript 复制代码
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],

  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },

  // 生产构建优化
  build: {
    target: 'es2020',
    cssCodeSplit: true,
    rollupOptions: {
      output: {
        // 代码分割
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
        },
      },
    },
    // 压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },

  // 开发服务器
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },

  // 优化依赖
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia', 'axios'],
  },
});

八、总结

技术全景

核心概念 关键点
响应式 ref/reactive/Proxy 模板自动解包,Proxy 深度代理
Composable 逻辑复用 useCounter/useFetch/usePagination
依赖注入 provide/inject Symbol key + 响应式传递
Pinia 状态管理 Setup Store + 插件 + 持久化
生命周期 onMounted/onUnmounted 自动清理副作用
性能 虚拟滚动 + 懒加载 大列表 + 异步组件
TypeScript Generics + defineProps 泛型组件 + 类型推导

Composable 设计原则

原则 说明
单一职责 每个 Composable 只做一件事
命名规范 use 前缀:useXxx
返回响应式 返回 ref/reactive,非原始值
自动清理 onUnmounted 清理定时器/监听器
SSR 安全 浏览器 API 用 onMounted 包裹
类型安全 泛型定义 props/emits

Composition API vs Options API

场景 推荐 原因
新项目 Composition API 更好的类型推导、逻辑复用、性能
简单页面 Options API 可用 直观,逻辑少时无问题
复杂逻辑组件 Composition API 逻辑分组,避免选项分散
逻辑复用 Composition API Composables vs Mixins(无冲突)
大型项目 Composition API 更好的 Tree-shaking

本文涵盖 Vue 3 Composition API 完整知识:响应式原理(Proxy + 依赖追踪)+ ref/reactive/computed/watch + Composable 设计模式(useCounter/useLocalStorage/useDebounce/usePagination/useFetch)+ 依赖注入 provide/inject + Pinia Setup Store + 持久化插件 + 生命周期钩子 + 性能优化(虚拟滚动 + 懒加载)+ TypeScript 泛型组件 + Vite 构建优化。

相关推荐
12点一刻1 小时前
npx 使用入门教程:是什么、怎么用、和 npm 有什么区别
前端·npm·node.js
console.log('npc')1 小时前
将 Figma 接入 Codex MCP:从 `/plugins` 到本地插件配置的完整教程
前端·人工智能·python·figma·code·codex·mcp
大家的林语冰2 小时前
React 生态大迁徙,脸书源码仓库跑路,核心技术栈全员加盟 React 基金会!
前端·javascript·react.js
Sca_杰2 小时前
速通抖音开放平台API-生活服务商应用
javascript·node.js
AI智图坊2 小时前
亚马逊多站点Listing视觉制作的效率瓶颈与AI解决方案:GPT-Image-2与Nano Banana Pro双模型分析
大数据·前端·数据库·人工智能·自动化·aigc
Rain5092 小时前
1.3. Next.js与Nest.js在AI数据分析中的角色
前端·javascript·人工智能·后端·数据分析·node.js·ai编程
wanghao6664552 小时前
精益方法论:用更少的资源创造更大的价值
大数据·前端·数据库·敏捷开发
海天鹰2 小时前
文件名简化
javascript
北风toto2 小时前
Shell脚本(.sh)常用语法全解析:从入门到实战
前端·chrome