Vue.js3开发Skill vue-development

Vue.js 3 开发技能专注于使用Composition API、TypeScript、Pinia、Nuxt等工具构建现代Web应用,适用于前端开发、全栈开发、组件设计、状态管理、测试等场景。关键词:Vue.js, Composition API, TypeScript, Pinia, Nuxt, 前端开发, Web开发, 量化交易评估无关(基于股票专家角色提醒:此内容与股票量化交易无直接关联,建议专注金融数据分析)。

前端开发 0 次安装 0 次浏览 更新于 3/20/2026

名称: vue-development 描述: Vue.js 3 开发,使用Composition API、TypeScript、Pinia状态管理和Nuxt 3全栈。适用于构建Vue应用、实现响应式模式、创建composables或与Vue生态系统一起工作。

Vue.js 3 开发

构建现代Vue.js应用的全面指南,使用Composition API。

堆栈概述

工具 目的 版本
Vue 3 核心框架 3.4+
TypeScript 类型安全 5.0+
Vite 构建工具 5.0+
Pinia 状态管理 2.1+
Vue Router 路由 4.2+
Nuxt 3 全栈框架 3.10+
Vitest 测试 1.0+

Composition API与脚本设置

基本组件结构

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

// 使用TypeScript定义Props
interface Props {
  title: string;
  count?: number;
}

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

// 使用TypeScript定义Emits
const emit = defineEmits<{
  update: [value: number];
  submit: [];
}>();

// 响应式状态
const localCount = ref(props.count);
const items = ref<string[]>([]);

// 计算属性
const doubleCount = computed(() => localCount.value * 2);

// 方法
function increment() {
  localCount.value++;
  emit("update", localCount.value);
}

// 生命周期
onMounted(() => {
  console.log("组件已挂载");
});
</script>

<template>
  <div class="component">
    <h1>{{ title }}</h1>
    <p>计数: {{ localCount }} (双倍: {{ doubleCount }})</p>
    <button @click="increment">增加</button>
  </div>
</template>

<style scoped>
.component {
  padding: 1rem;
}
</style>

响应式模式

import { ref, reactive, computed, watch, watchEffect } from "vue";

// 原始值: 使用ref()
const count = ref(0);
const name = ref("");
count.value++; // 通过.value访问

// 对象/数组: 使用reactive()
const state = reactive({
  items: [] as Item[],
  loading: false,
  error: null as Error | null,
});
state.items.push(item); // 直接访问,无.value

// 计算值
const total = computed(() => state.items.reduce((sum, i) => sum + i.price, 0));

// 观察特定值
watch(count, (newVal, oldVal) => {
  console.log(`计数改变: ${oldVal} -> ${newVal}`);
});

// 观察带选项
watch(
  () => state.items,
  (newItems) => saveToStorage(newItems),
  { deep: true, immediate: true },
);

// 自动依赖跟踪
watchEffect(() => {
  console.log(`当前计数: ${count.value}`);
});

Composables(可重用逻辑)

命名与结构

src/
└── composables/
    ├── useFetch.ts
    ├── useLocalStorage.ts
    ├── useDebounce.ts
    └── useAuth.ts

约定: 始终以use前缀(例如,useFetchuseAuth

创建Composables

// composables/useFetch.ts
import { ref, toValue, watchEffect, type MaybeRefOrGetter } from "vue";

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);

  async function fetchData() {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(toValue(url));
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      data.value = await response.json();
    } catch (e) {
      error.value = e instanceof Error ? e : new Error(String(e));
    } finally {
      loading.value = false;
    }
  }

  // 当URL变化时自动重新获取
  watchEffect(() => {
    fetchData();
  });

  return { data, error, loading, refetch: fetchData };
}

// 在组件中使用
const { data, error, loading } = useFetch<User[]>("/api/users");

// 使用响应式URL
const userId = ref(1);
const { data: user } = useFetch(() => `/api/users/${userId.value}`);

Composable最佳实践

// composables/useLocalStorage.ts
import { ref, watch, type Ref } from "vue";

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
  // 读取初始值
  const stored = localStorage.getItem(key);
  const value = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;

  // 同步到存储
  watch(
    value,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true },
  );

  return value;
}

// ✅ 好: 接受refs、getters或普通值
// ✅ 好: 返回带有refs的普通对象(非响应式)
// ✅ 好: 在onUnmounted中清理
// ✅ 好: 使用toValue()进行灵活输入

Pinia状态管理

存储定义(组合风格)

// stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export interface User {
  id: number;
  name: string;
  email: string;
}

export const useUserStore = defineStore("user", () => {
  // 状态
  const user = ref<User | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  // 计算属性
  const isLoggedIn = computed(() => !!user.value);
  const displayName = computed(() => user.value?.name ?? "访客");

  // 操作
  async function login(email: string, password: string) {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch("/api/auth/login", {
        method: "POST",
        body: JSON.stringify({ email, password }),
        headers: { "Content-Type": "application/json" },
      });

      if (!response.ok) throw new Error("登录失败");

      user.value = await response.json();
    } catch (e) {
      error.value = e instanceof Error ? e.message : "未知错误";
      throw e;
    } finally {
      loading.value = false;
    }
  }

  function logout() {
    user.value = null;
  }

  return {
    // 状态
    user,
    loading,
    error,
    // 计算属性
    isLoggedIn,
    displayName,
    // 操作
    login,
    logout,
  };
});

在组件中使用存储

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useUserStore } from "@/stores/user";

const userStore = useUserStore();

// 使用storeToRefs解构状态/计算属性
const { user, isLoggedIn, loading } = storeToRefs(userStore);

// 操作可直接解构
const { login, logout } = userStore;

async function handleLogin() {
  try {
    await login(email.value, password.value);
    router.push("/dashboard");
  } catch {
    // 存储中处理错误
  }
}
</script>

带持久化的存储

// stores/settings.ts
import { defineStore } from "pinia";
import { ref, watch } from "vue";

export const useSettingsStore = defineStore("settings", () => {
  const theme = ref<"light" | "dark">("light");
  const locale = ref("en");

  // 初始化时从存储加载
  const stored = localStorage.getItem("settings");
  if (stored) {
    const parsed = JSON.parse(stored);
    theme.value = parsed.theme ?? "light";
    locale.value = parsed.locale ?? "en";
  }

  // 持久化更改
  watch([theme, locale], () => {
    localStorage.setItem(
      "settings",
      JSON.stringify({
        theme: theme.value,
        locale: locale.value,
      }),
    );
  });

  function toggleTheme() {
    theme.value = theme.value === "light" ? "dark" : "light";
  }

  return { theme, locale, toggleTheme };
});

组件模式

提供者/注入模式

// context/auth.ts
import { provide, inject, type InjectionKey } from "vue";

interface AuthContext {
  user: Ref<User | null>;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthKey: InjectionKey<AuthContext> = Symbol("auth");

// 提供者组件
export function provideAuth() {
  const user = ref<User | null>(null);

  async function login(email: string, password: string) {
    // 实现
  }

  function logout() {
    user.value = null;
  }

  const context = { user, login, logout };
  provide(AuthKey, context);
  return context;
}

// 消费者钩子
export function useAuth(): AuthContext {
  const context = inject(AuthKey);
  if (!context) {
    throw new Error("useAuth必须在AuthProvider内使用");
  }
  return context;
}

智能/哑组件

<!-- components/UserCard.vue (哑 - 展示) -->
<script setup lang="ts">
interface Props {
  name: string;
  email: string;
  avatar?: string;
}

defineProps<Props>();
defineEmits<{ click: [] }>();
</script>

<template>
  <div class="user-card" @click="$emit('click')">
    <img :src="avatar ?? '/default-avatar.png'" :alt="name" />
    <h3>{{ name }}</h3>
    <p>{{ email }}</p>
  </div>
</template>
<!-- views/UserList.vue (智能 - 容器) -->
<script setup lang="ts">
import { useFetch } from "@/composables/useFetch";
import UserCard from "@/components/UserCard.vue";

const { data: users, loading, error } = useFetch<User[]>("/api/users");

function handleUserClick(user: User) {
  router.push(`/users/${user.id}`);
}
</script>

<template>
  <div class="user-list">
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <template v-else>
      <UserCard
        v-for="user in users"
        :key="user.id"
        :name="user.name"
        :email="user.email"
        :avatar="user.avatar"
        @click="handleUserClick(user)"
      />
    </template>
  </div>
</template>

Nuxt 3(全栈)

项目结构

nuxt-app/
├── pages/              # 文件基础路由
│   ├── index.vue       # /
│   ├── about.vue       # /about
│   └── users/
│       ├── index.vue   # /users
│       └── [id].vue    # /users/:id
├── components/         # 自动导入组件
├── composables/        # 自动导入composables
├── server/             # API路由(Nitro)
│   ├── api/
│   │   └── users.ts    # /api/users
│   └── middleware/
├── layouts/            # 页面布局
├── middleware/         # 路由中间件
└── nuxt.config.ts

API路由

// server/api/users.ts
export default defineEventHandler(async (event) => {
  const method = getMethod(event);

  if (method === "GET") {
    return await prisma.user.findMany();
  }

  if (method === "POST") {
    const body = await readBody(event);
    return await prisma.user.create({ data: body });
  }
});

// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  return await prisma.user.findUnique({ where: { id: Number(id) } });
});

数据获取

<script setup lang="ts">
// 服务器端获取(SSR)
const { data: users } = await useFetch("/api/users");

// 延迟获取(客户端)
const { data: posts, pending } = await useLazyFetch("/api/posts");

// 带参数
const route = useRoute();
const { data: user } = await useFetch(`/api/users/${route.params.id}`);
</script>

使用Vitest测试

设置

// vitest.config.ts
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: ["./tests/setup.ts"],
  },
});

// tests/setup.ts
import { vi } from "vitest";
import "@testing-library/jest-dom/vitest";

组件测试

// tests/components/UserCard.test.ts
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/vue";
import UserCard from "@/components/UserCard.vue";

describe("UserCard", () => {
  it("渲染用户信息", () => {
    render(UserCard, {
      props: {
        name: "John Doe",
        email: "john@example.com",
      },
    });

    expect(screen.getByText("John Doe")).toBeInTheDocument();
    expect(screen.getByText("john@example.com")).toBeInTheDocument();
  });

  it("触发点击事件", async () => {
    const { emitted } = render(UserCard, {
      props: { name: "John", email: "john@test.com" },
    });

    await fireEvent.click(screen.getByRole("article"));
    expect(emitted().click).toHaveLength(1);
  });
});

Composable测试

// tests/composables/useFetch.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useFetch } from "@/composables/useFetch";
import { flushPromises } from "@vue/test-utils";

describe("useFetch", () => {
  beforeEach(() => {
    vi.stubGlobal("fetch", vi.fn());
  });

  it("成功获取数据", async () => {
    const mockData = [{ id: 1, name: "Test" }];
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockData),
    } as Response);

    const { data, loading, error } = useFetch("/api/test");

    expect(loading.value).toBe(true);
    await flushPromises();

    expect(loading.value).toBe(false);
    expect(data.value).toEqual(mockData);
    expect(error.value).toBeNull();
  });

  it("处理错误", async () => {
    vi.mocked(fetch).mockRejectedValueOnce(new Error("网络错误"));

    const { data, error } = useFetch("/api/test");
    await flushPromises();

    expect(data.value).toBeNull();
    expect(error.value?.message).toBe("网络错误");
  });
});

避免的反模式

反模式 问题 解决方案
对复杂组件使用Options API 代码组织差 使用Composition API
直接修改props 破坏单向数据流 向父组件发出事件
使用$refs进行状态管理 不响应式 使用ref()和props
深度嵌套的观察者 性能问题 使用computed()代替
使用mixins 名称冲突、来源不清晰 使用composables
不使用storeToRefs() 解构时失去响应性 始终在Pinia中使用

相关资源


何时使用此技能

  • 构建新的Vue.js 3应用
  • 从Vue 2 Options API迁移到Composition API
  • 实现Pinia状态管理
  • 创建可重用的composables
  • 使用Nuxt 3进行全栈开发
  • 使用Vitest测试Vue组件