10. Vue3 + TypeScript:完整集成指南

Vue3 + TypeScript:完整集成指南

Vue3 TypeScript

恭喜你走到最后一期!前面我们学习了 ES6、Vite、Vue3 基础、组合式 API 和 TypeScript。现在,让我们把 TypeScript 完整集成到 Vue3 项目中。

项目创建

Vite + Vue + TypeScript

npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install

项目结构

src/
├── main.ts
├── App.vue
├── vite-env.d.ts
├── components/
│   └── HelloWorld.vue
├── views/
├── router/
│   └── index.ts
├── stores/
│   └── user.ts
├── types/
│   └── index.ts
├── api/
│   └── user.ts
├── composables/
│   └── useUser.ts
└── utils/
    └── format.ts

类型定义

全局类型

// types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
}

export interface ApiResponse {
  code: number
  message: string
  data: T
}

export type Theme = 'light' | 'dark'

export interface AppState {
  user: User | null
  theme: Theme
  loading: boolean
}

组件 Props 类型


interface Props {
  title: string
  count?: number
  users: User[]
}

const props = withDefaults(defineProps(), {
  count: 0
})

组件 Emits 类型


interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'change', event: Event): void
}

const emit = defineEmits()

function handleUpdate() {
  emit('update', 'new value')
}

组合式 API + TypeScript

ref

import { ref } from 'vue'

// 类型推断
const count = ref(0)
const name = ref('Alice')

// 显式类型
const user = ref(null)
const items = ref([])

reactive

import { reactive } from 'vue'

interface State {
  count: number
  name: string
  items: string[]
}

const state = reactive({
  count: 0,
  name: '',
  items: []
})

computed

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 自动推断返回类型
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 显式类型
const userCount = computed(() => users.value.length)

组合式函数

// composables/useUser.ts
import { ref, computed } from 'vue'
import type { User } from '@/types'

export function useUser() {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const isLoggedIn = computed(() => !!user.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  async function fetchUser(id: number) {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(<code>/api/users/${id})
      user.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
  }

  return {
    user,
    loading,
    error,
    isLoggedIn,
    userName,
    fetchUser,
    logout
  }
}

Pinia + TypeScript

Store 类型定义

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

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    const data = await response.json()
    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
}, {
  persist: true
})

使用 Store


import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// Actions
const { login, logout } = userStore

// 表单
const email = ref('')
const password = ref('')

async function handleLogin() {
  await login(email.value, password.value)
}

  <div>
    <p>Welcome, {{ userName }}</p>
    <button>Logout</button>
  </div>

    <button type="submit">Login</button>

Vue Router + TypeScript

路由类型

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true,
    meta: {
      requiresAuth: true
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

路由元信息类型

// types/router.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
  }
}

使用路由


import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 获取参数(类型安全)
const userId = computed(() => Number(route.params.id))

// 编程式导航
function goToUser(id: number) {
  router.push({ name: 'User', params: { id } })
}

API 集成

类型安全的 API 客户端

// api/client.ts
import type { ApiResponse } from '@/types'

class ApiClient {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  private async request(
    path: string,
    options?: RequestInit
  ): Promise {
    const response = await fetch(<code>${this.baseUrl}${path}, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      }
    })

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

    return response.json()
  }

  get(path: string): Promise {
    return this.request(path)
  }

  post(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  put(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'PUT',
      body: JSON.stringify(data)
    })
  }

  delete(path: string): Promise {
    return this.request(path, {
      method: 'DELETE'
    })
  }
}

export const api = new ApiClient('/api')

API 模块

// api/user.ts
import { api } from './client'
import type { User, ApiResponse } from '@/types'

export const userApi = {
  getAll(): Promise {
    return api.get<ApiResponse>('/users').then(r => r.data)
  },

  getById(id: number): Promise {
    return api.get<ApiResponse>(<code>/users/${id}).then(r => r.data)
  },

  create(data: Omit): Promise {
    return api.post<ApiResponse>('/users', data).then(r => r.data)
  },

  update(id: number, data: Partial): Promise {
    return api.put<ApiResponse>(/users/${id}, data).then(r => r.data)
  },

  delete(id: number): Promise {
    return api.delete(/users/${id})
  }
}

使用 API


import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import type { User } from '@/types'

const users = ref([])
const loading = ref(false)
const error = ref(null)

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

  try {
    users.value = await userApi.getAll()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Unknown error'
  } finally {
    loading.value = false
  }
}

onMounted(fetchUsers)

  <div>Loading...</div>
  <div>{{ error }}</div>
  <ul>
    <li>
      {{ user.name }}
    </li>
  </ul>

表单处理

类型安全的表单


import { ref, reactive } from 'vue'

interface FormData {
  name: string
  email: string
  age: number
  subscribe: boolean
}

interface FormErrors {
  name?: string
  email?: string
  age?: string
}

const form = reactive({
  name: '',
  email: '',
  age: 0,
  subscribe: false
})

const errors = reactive({})

function validate(): boolean {
  let isValid = true

  if (!form.name.trim()) {
    errors.name = 'Name is required'
    isValid = false
  }

  if (!form.email.includes('@')) {
    errors.email = 'Invalid email'
    isValid = false
  }

  if (form.age  150) {
    errors.age = 'Invalid age'
    isValid = false
  }

  return isValid
}

async function handleSubmit() {
  if (!validate()) return

  // 提交表单
  await submitForm(form)
}

    <div>

      <span>{{ errors.name }}</span>
    </div>

    <div>

      <span>{{ errors.email }}</span>
    </div>

    <div>

      <span>{{ errors.age }}</span>
    </div>

    <div>
      <label>

        Subscribe to newsletter
      </label>
    </div>

    <button type="submit">Submit</button>

使用 vee-validate

npm install vee-validate zod @vee-validate/zod

import { useForm, useField } from 'vee-validate'
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, 'Name is required'),
    email: z.string().email('Invalid email'),
    age: z.number().min(0).max(150)
  })
)

const { handleSubmit, errors } = useForm({
  validationSchema: schema
})

const { value: name } = useField('name')
const { value: email } = useField('email')
const { value: age } = useField('age')

const onSubmit = handleSubmit((values) => {
  console.log(values)
})

工具类型

组件实例类型

import type { ComponentInstance } from 'vue'

// 获取组件实例类型
type MyComponentInstance = ComponentInstance

提取 Props 类型

import type { ExtractPropTypes } from 'vue'

const propsDefinition = {
  title: String,
  count: {
    type: Number,
    default: 0
  }
} as const

type Props = ExtractPropTypes
// { title: string; count: number }

组件自定义类型

// 自定义组件类型
declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    MyButton: typeof import('@/components/MyButton.vue')['default']
    MyInput: typeof import('@/components/MyInput.vue')['default']
  }
}

测试

Vitest 配置

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

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true
  }
})

组件测试

// components/__tests__/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../Button.vue'

describe('Button', () => {
  it('renders with text', () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

最佳实践

1. 使用 script setup


// 推荐

2. 类型优先

// 先定义类型,再实现
interface User {
  id: number
  name: string
}

const user = ref(null)

3. 使用组合式函数

// 提取可复用逻辑
export function useUser() {
  // ...
}

4. 严格模式

// tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

5. 类型安全的 Props


interface Props {
  title: string
  count?: number
}

const props = withDefaults(defineProps(), {
  count: 0
})

系列总结

恭喜你完成了从 ES6 到 Vue3 + TypeScript 的完整学习之旅!

第一期:TypeScript 基础类型系统
第二期:接口、泛型与类型体操
第三期:项目配置与最佳实践
第四期:TypeScript 与前端框架
第五期:Vue3 + TypeScript 完整集成

你现在掌握了:

  • TypeScript 类型系统
  • Vue3 组合式 API
  • 类型安全的组件开发
  • Pinia 状态管理
  • Vue Router 类型安全
  • API 集成最佳实践

下一步

  • 学习 Nuxt 3(Vue3 全栈框架)
  • 探索 Vite 插件开发
  • 学习服务端渲染
  • 阅读官方文档

进阶资源


恭喜!你已经掌握了 Vue3 + TypeScript 的核心技能。去构建你的下一个项目吧!

Views: 10

05. Vue3生态实战:Router、Pinia与完整项目

Vue3生态实战:Router、Pinia与完整项目

Vue Ecosystem

恭喜你走到最后一期!前四期我们学习了ES6、Vite、Vue3基础和组合式API。现在,让我们把这些知识整合起来,构建一个完整的Vue3应用。

这一期,我们学习Vue Router(路由)和Pinia(状态管理),然后做一个实战项目。

Vue Router

安装

pnpm add vue-router

基础配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue') // 懒加载
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  <router-view />
</template>

路由参数

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

// 获取参数
console.log(route.params.id) // /users/123 → 123
console.log(route.query.page) // /users?page=2 → 2
</script>

编程式导航

<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

function goToUser(id) {
  router.push({ name: 'User', params: { id } })
}

function goBack() {
  router.back()
}

function replace() {
  router.replace({ name: 'Home' }) // 不留历史记录
}
</script>

嵌套路由

const routes = [
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue'),
    children: [
      {
        path: '', // /settings
        component: () => import('@/views/SettingsProfile.vue')
      },
      {
        path: 'account', // /settings/account
        component: () => import('@/views/SettingsAccount.vue')
      },
      {
        path: 'security', // /settings/security
        component: () => import('@/views/SettingsSecurity.vue')
      }
    ]
  }
]
<!-- Settings.vue -->
<template>
  <div>
    <h1>Settings</h1>
    <nav>
      <router-link to="/settings">Profile</router-link>
      <router-link to="/settings/account">Account</router-link>
      <router-link to="/settings/security">Security</router-link>
    </nav>
    <router-view />
  </div>
</template>

导航守卫

// 全局前置守卫
router.beforeEach((to, from) => {
  // 需要登录
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// 全局后置守卫
router.afterEach((to) => {
  document.title = to.meta.title || 'My App'
})
// 路由元信息
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: 'Admin Dashboard'
    }
  }
]

Pinia

Pinia是Vue3官方推荐的状态管理库,比Vuex更简单、更强大。

安装

pnpm add pinia

基础配置

// src/stores/index.js
import { createPinia } from 'pinia'

const pinia = createPinia()
export default pinia
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

定义Store

// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 组合式API风格(推荐)
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

使用Store

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

const userStore = useUserStore()

// 解构state和getters(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// actions直接解构
const { login, logout } = userStore

// 直接访问
console.log(userStore.user)
</script>

<template>
  <div v-if="isLoggedIn">
    Welcome, {{ userName }}
    <button @click="logout">Logout</button>
  </div>
  <div v-else>
    <button @click="login">Login</button>
  </div>
</template>

选项式API风格

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

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    },

    async fetchCount() {
      const response = await fetch('/api/count')
      this.count = await response.json()
    }
  }
})

持久化

// 使用pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: true // 启用持久化
})

实战项目:任务管理应用

让我们把学到的知识整合起来,做一个任务管理应用。

项目结构

src/
├── main.js
├── App.vue
├── router/
│   └── index.js
├── stores/
│   ├── index.js
│   ├── tasks.js
│   └── user.js
├── composables/
│   └── useLocalStorage.js
├── views/
│   ├── Home.vue
│   ├── Login.vue
│   └── Tasks.vue
└── components/
    ├── TaskItem.vue
    └── TaskForm.vue

任务Store

// src/stores/tasks.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref([
    { id: 1, title: 'Learn Vue3', completed: false },
    { id: 2, title: 'Build a project', completed: false }
  ])

  const filter = ref('all') // all, active, completed

  const filteredTasks = computed(() => {
    if (filter.value === 'active') {
      return tasks.value.filter(t => !t.completed)
    }
    if (filter.value === 'completed') {
      return tasks.value.filter(t => t.completed)
    }
    return tasks.value
  })

  const remainingCount = computed(() =>
    tasks.value.filter(t => !t.completed).length
  )

  function addTask(title) {
    tasks.value.push({
      id: Date.now(),
      title,
      completed: false
    })
  }

  function removeTask(id) {
    const index = tasks.value.findIndex(t => t.id === id)
    if (index > -1) {
      tasks.value.splice(index, 1)
    }
  }

  function toggleTask(id) {
    const task = tasks.value.find(t => t.id === id)
    if (task) {
      task.completed = !task.completed
    }
  }

  function clearCompleted() {
    tasks.value = tasks.value.filter(t => !t.completed)
  }

  return {
    tasks,
    filter,
    filteredTasks,
    remainingCount,
    addTask,
    removeTask,
    toggleTask,
    clearCompleted
  }
}, {
  persist: true
})

任务列表组件

<!-- src/views/Tasks.vue -->
<script setup>
import { useTaskStore } from '@/stores/tasks'
import { storeToRefs } from 'pinia'
import TaskItem from '@/components/TaskItem.vue'
import TaskForm from '@/components/TaskForm.vue'

const taskStore = useTaskStore()
const { filteredTasks, filter, remainingCount } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask, clearCompleted } = taskStore
</script>

<template>
  <div class="tasks">
    <h1>Tasks</h1>
    <TaskForm @add="addTask" />

    <ul class="task-list">
      <TaskItem
        v-for="task in filteredTasks"
        :key="task.id"
        :task="task"
        @toggle="toggleTask(task.id)"
        @remove="removeTask(task.id)"
      />
    </ul>

    <div class="filters">
      <button
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        All
      </button>
      <button
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        Active
      </button>
      <button
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        Completed
      </button>
    </div>

    <div class="footer">
      {{ remainingCount }} items left
      <button @click="clearCompleted">Clear completed</button>
    </div>
  </div>
</template>

<style scoped>
.tasks {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.filters button.active {
  font-weight: bold;
}

.task-list {
  list-style: none;
  padding: 0;
}
</style>

任务项组件

<!-- src/components/TaskItem.vue -->
<script setup>
defineProps({
  task: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <li class="task-item" :class="{ completed: task.completed }">
    <input
      type="checkbox"
      :checked="task.completed"
      @change="emit('toggle')"
    />
    <span>{{ task.title }}</span>
    <button @click="emit('remove')">×</button>
  </li>
</template>

<style scoped>
.task-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-item.completed span {
  text-decoration: line-through;
  color: #999;
}
</style>

任务表单组件

<!-- src/components/TaskForm.vue -->
<script setup>
import { ref } from 'vue'

const title = ref('')

const emit = defineEmits(['add'])

function handleSubmit() {
  if (title.value.trim()) {
    emit('add', title.value.trim())
    title.value = ''
  }
}
</script>

<template>
  <form class="task-form" @submit.prevent="handleSubmit">
    <input
      v-model="title"
      type="text"
      placeholder="What needs to be done?"
    />
    <button type="submit">Add</button>
  </form>
</template>

<style scoped>
.task-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.task-form input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
}
</style>

项目优化

代码分割

// router/index.js
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
  }
]

懒加载组件

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

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

构建优化

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

系列总结

恭喜你完成了现代前端开发到Vue3的完整学习之旅!

第一期:ES6+、Node.js、npm基础
第二期:Vite构建工具
第三期:Vue3响应式和组件
第四期:组合式API和逻辑复用
第五期:Router、Pinia和实战项目

你现在掌握了:

  • 现代JavaScript语法
  • Vite项目构建
  • Vue3核心概念
  • 组合式API和组合式函数
  • Vue Router路由管理
  • Pinia状态管理
  • 完整项目开发流程

下一步

  • 学习TypeScript(Vue3的最佳伴侣)
  • 探索UI框架(Element Plus、Naive UI)
  • 学习服务端渲染(Nuxt 3)
  • 阅读官方文档(https://vuejs.org/

进阶资源


恭喜!你已经掌握了现代前端开发到Vue3的核心技能。去构建你的下一个项目吧!

Views: 15

04. Vue3组合式API:代码组织的艺术

Vue3组合式API:代码组织的艺术

Composition API

如果你写过大型Vue2项目,一定遇到过这样的困扰:

  • 一个组件几千行,data、methods、computed分散各处
  • 想复用一段逻辑,只能用mixin,然后命名冲突
  • TypeScript支持不完善,类型推断总有问题

组合式API(Composition API)就是为了解决这些问题而生的。它让你按功能组织代码,而不是按选项组织。

选项式API vs 组合式API

选项式API(Options API)


export default {
  data() {
    return {
      count: 0,
      user: null
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    fetchUser() {
      // ...
    }
  },
  mounted() {
    this.fetchUser()
  }
}

问题:

  • 相关代码分散在不同选项中
  • 逻辑复用困难(mixin有命名冲突)
  • TypeScript类型推断不好

组合式API(Composition API)


import { ref, computed, onMounted } from 'vue'

// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
  count.value++
}

// 用户逻辑
const user = ref(null)
async function fetchUser() {
  user.value = await fetch('/api/user').then(r => r.json())
}
onMounted(fetchUser)

优势:

  • 相关代码放在一起
  • 逻辑可以提取成独立函数
  • 完美的TypeScript支持

setup函数

``是setup函数的语法糖:

<!-- 等价于 -->

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}

<!-- 语法糖 -->

const count = ref(0)

所有顶层绑定自动暴露给模板。

Props和Emits


// 定义props
const props = defineProps({
  title: String
})

// 定义emits
const emit = defineEmits(['update'])

// 使用
console.log(props.title)
emit('update', data)

响应式转换

Vue3提供响应式语法糖(实验性):


// 启用后,ref可以省略.value
let count = $ref(0)
count++  // 自动处理

// 解构保持响应性
const { x, y } = $(useMouse())

组合式函数

组合式函数(Composables)是Vue3复用逻辑的核心方式。

基础示例

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}
<!-- 使用 -->

import { useCounter } from './useCounter'

const { count, doubleCount, increment } = useCounter(10)

  <p>Count: {{ count }}</p>
  <p>Double: {{ doubleCount }}</p>
  <button>+1</button>

实用组合式函数

useMouse - 追踪鼠标位置:

import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

useFetch - 数据获取:

import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

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

    try {
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 支持响应式URL
  watchEffect(fetch)

  return { data, error, loading, refetch: fetch }
}

import { useFetch } from './useFetch'

const { data, loading, error } = useFetch('/api/users')

  <div>Loading...</div>
  <div>Error: {{ error.message }}</div>
  <div>
    <ul>
      <li>
        {{ user.name }}
      </li>
    </ul>
  </div>

useLocalStorage - 本地存储:

import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    (newVal) => {
      if (newVal === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newVal))
      }
    },
    { deep: true }
  )

  return data
}

import { useLocalStorage } from './useLocalStorage'

const theme = useLocalStorage('theme', 'light')

  <button>
    Toggle Theme
  </button>

provide/inject

跨层级组件通信:

<!-- 祖先组件 -->

import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
})
<!-- 后代组件(任意层级) -->

import { inject } from 'vue'

const theme = inject('theme')
const toggleTheme = inject('toggleTheme')

  <div>
    <button>Toggle</button>
  </div>

响应式provide

// 使用Symbol避免命名冲突
export const ThemeKey = Symbol('theme')

// 提供默认值
const theme = inject(ThemeKey, 'light')

// 只读
provide(ThemeKey, readonly(theme))

模板引用


import { ref, onMounted } from 'vue'

const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})

v-for中的ref


const listRef = ref([])

function setRef(el) {
  if (el) {
    listRef.value.push(el)
  }
}

  <div>
    {{ item }}
  </div>

组件ref


import ChildComponent from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  childRef.value.someMethod()
}

异步组件


import { defineAsyncComponent } from 'vue'

// 异步加载
const AsyncComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

最佳实践

命名约定

// 组合式函数以use开头
useCounter()
useMouse()
useFetch()

// 私有函数以_开头或使用__
function _internalHelper() {}

返回值

// 返回响应式引用
export function useCounter() {
  const count = ref(0)
  return { count }  // 不要返回 count.value
}

// 使用时保持响应性
const { count } = useCounter()
// count是ref,仍然是响应式的

清理副作用

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))

  // 或者返回清理函数
  return () => target.removeEventListener(event, callback)
}

// 使用
const cleanup = useEventListener(window, 'resize', handleResize)
cleanup()  // 手动清理

参数处理

// 支持ref和原始值
import { toValue } from 'vue'

export function useFetch(url) {
  // toValue会解包ref
  const finalUrl = toValue(url)

  watchEffect(() => {
    fetch(toValue(url))  // 响应式URL
  })
}

// 使用
const url = ref('/api/users')
const { data } = useFetch(url)  // url变化时自动重新fetch

小结

你现在掌握了:

  • 组合式API的核心思想(按功能组织代码)
  • 组合式函数的编写和使用
  • provide/inject跨层级通信
  • 模板引用和异步组件
  • 最佳实践和命名约定

下一期,我们进入Vue3生态,学习Vue Router路由和Pinia状态管理,完成一个实战项目。


练习任务

  1. 编写一个useCounter组合式函数
  2. 编写一个useDebounce防抖函数
  3. 使用provide/inject实现主题切换
  4. 实现一个异步加载的组件

下期预告:《Vue3生态:Router、Pinia与实战项目》—— 路由怎么配?状态怎么管?来做一个完整项目!

Views: 12

03. Vue3基础:响应式系统和组件入门

Vue3基础:响应式系统和组件入门

Vue Logo

终于进入Vue3的世界了!这一期,我们学习Vue3最核心的概念:响应式系统和组件。

理解这两个概念,你就掌握了Vue3的80%。

响应式系统

什么是响应式?

// 普通变量 - 不是响应式的
let count = 0
count++
// Vue不知道count变了,UI不会更新

// 响应式变量 - Vue会追踪变化
const count = ref(0)
count.value++
// Vue知道count变了,自动更新UI

响应式 = 数据变化时,UI自动更新。

ref vs reactive

Vue3提供两种响应式API:

ref:包装单个值

import { ref } from 'vue'

const count = ref(0)
const message = ref('Hello')
const user = ref({ name: 'Vue' })

// 读取和修改需要.value
console.log(count.value)  // 0
count.value++

reactive:包装对象/数组

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Vue'
  }
})

// 直接访问属性
console.log(state.count)  // 0
state.count++

// 数组
const list = reactive([1, 2, 3])
list.push(4)

怎么选?

场景 推荐
基本类型(数字、字符串) ref
对象 都可以
需要整体替换 ref
解构友好 reactive

我的建议:默认用ref,对象很大时用reactive

模板中自动解包


import { ref } from 'vue'

const count = ref(0)

  <!-- 不需要.value,自动解包 -->
  <p>Count: {{ count }}</p>
  <button>+1</button>

响应式丢失问题

// ❌ 解构会丢失响应性
const state = reactive({ count: 0 })
let { count } = state  // count是普通数字
count++  // 不会触发更新!

// ✅ 使用toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)  // count是ref
count.value++  // 会触发更新

// ✅ 使用toRef(单个属性)
import { toRef } from 'vue'
const count = toRef(state, 'count')

computed

计算属性:基于响应式数据派生新值,自动缓存。


import { ref, computed } from 'vue'

const firstName = ref('Vue')
const lastName = ref('3')

// 只读计算属性
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 可写计算属性
const fullName2 = computed({
  get: () => ${firstName.value} ${lastName.value},
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})

  

{{ fullName }}

watch

侦听数据变化:


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

const count = ref(0)

// watch:明确指定侦听源
watch(count, (newVal, oldVal) => {
  console.log(<code>count changed: ${oldVal} -> ${newVal})
})

// watch多个源
watch([count, anotherRef], ([newCount, newAnother]) => {
  // ...
})

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(count is ${count.value})
})

// 立即执行 + 可停止
const stop = watchEffect(() => {
  // ...
}, { immediate: true })

// 停止侦听
stop()

watch vs watchEffect

  • watch:明确指定侦听源,可访问新旧值
  • watchEffect:自动追踪,适合副作用

组件基础

定义组件


import { ref } from 'vue'

const count = ref(0)

  <button>
    Clicked {{ count }} times
  </button>

button {
  color: blue;
}

``是Vue3的语法糖,自动暴露变量给模板。

Props:父传子

<!-- Child.vue -->

// 定义props
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 使用props
console.log(props.title)

  <h1>{{ title }}</h1>
  <p>Count: {{ count }}</p>
<!-- Parent.vue -->

import Child from './Child.vue'

Emits:子传父

<!-- Child.vue -->

const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'Vue' })
}

  <button>Update</button>
<!-- Parent.vue -->

import Child from './Child.vue'

function onUpdate(data) {
  console.log('Received:', data)
}

v-model:双向绑定

<!-- 自定义组件支持v-model -->

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

Vue3.4+可以用defineModel


const modelValue = defineModel()

插槽

<!-- Card.vue -->

  <div class="card">
    <div class="header">

    </div>
    <div class="content">

    </div>
    <div class="footer">

    </div>
  </div>
<!-- 使用 -->

    <h1>Title</h1>

  <p>Content goes here</p>

    <button>OK</button>

作用域插槽:

<!-- List.vue -->

const items = ['Apple', 'Banana', 'Orange']

  <ul>
    <li>

    </li>
  </ul>
<!-- 使用 -->

    {{ index + 1 }}. {{ item }}

模板语法

文本插值


  <p>{{ message }}</p>
  <p>{{ count + 1 }}</p>
  <p>{{ formatDate(date) }}</p>

指令


  <!-- 条件渲染 -->
  <div>Type A</div>
  <div>Type B</div>
  <div>Other</div>

  <!-- 列表渲染 -->
  <ul>
    <li>
      {{ item.name }}
    </li>
  </ul>

  <!-- 事件绑定 -->
  <button>Click</button>
  <button>+1</button>
  ...

  <!-- 属性绑定 -->
  <img />
  <div>...</div>
  <div>...</div>

  <!-- 双向绑定 -->

  ...

  <!-- 显示/隐藏 -->
  <div>...</div>

v-if vs v-show

特性 v-if v-show
渲染方式 条件为假时不渲染 始终渲染,用CSS隐藏
切换成本 高(销毁/重建) 低(只改CSS)
初始成本 低(不渲染) 高(总是渲染)

建议:频繁切换用v-show,条件很少改变用v-if

Class和Style绑定


  <!-- 对象语法 -->
  <div></div>

  <!-- 数组语法 -->
  <div></div>

  <!-- 混合 -->
  <div></div>

  <!-- Style对象 -->
  <div></div>

  <!-- Style数组 -->
  <div></div>

生命周期


import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})

onUpdated(() => {
  console.log('组件已更新')
})

onUnmounted(() => {
  console.log('组件已卸载')
})

生命周期钩子:

选项式API 组合式API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

小结

你现在掌握了:

  • 响应式系统(ref、reactive、computed、watch)
  • 组件通信(props、emits、v-model、插槽)
  • 模板语法(指令、条件、循环)
  • 生命周期钩子

下一期,我们深入学习组合式API - Vue3最强大的特性。你会学会如何组织代码、复用逻辑、提取组合式函数。


练习任务

  1. 创建一个计数器组件,用ref实现
  2. 创建一个父子组件,通过props和emits通信
  3. 实现一个自定义v-model组件
  4. 使用作用域插槽渲染列表

下期预告:《Vue3组合式API:代码组织的艺术》—— 组合式函数怎么写?如何复用逻辑?

Views: 14

01. 现代前端开发入门:从ES6到Vue3的第一步

现代前端开发入门:从ES6到Vue3的第一步

Modern Frontend

你刚决定学习前端开发,打开教程一看:ES6、Node.js、npm、Webpack、Vite、TypeScript...这些名词像天书一样砸过来。别慌,这篇文章帮你理清现代前端的"基础设施",为Vue3开发打下坚实基础。

现代前端开发三件套

在Vue3之前,你需要先掌握三个基础:

  1. ES6+语法 - 现代JavaScript的写法
  2. Node.js + npm - 包管理和构建工具的基础
  3. 模块化开发 - 代码组织方式

让我们一个个攻克。

ES6+必学语法

ES6(ECMAScript 2015)是JavaScript的重大升级,Vue3大量使用这些新特性。

let和const

// ❌ 旧写法
var name = 'Vue'
var name = 'React' // 可以重复声明,容易出bug

// ✅ 新写法
const app = 'Vue3' // 常量,不可重新赋值
let count = 0 // 变量,可以修改
count = 1 // OK
// app = 'React' // 报错!const不能重新赋值

规则:默认用const,需要修改时才用let,永远不用var

箭头函数

// ❌ 旧写法
const add = function(a, b) {
return a + b
}

// ✅ 新写法
const add = (a, b) => {
return a + b
}

// 更简洁:单行可以省略大括号和return
const add = (a, b) => a + b

// 单参数可以省略括号
const double = n => n * 2

关键区别:箭头函数没有自己的this,它会继承外层作用域的this。这在Vue中非常重要。

const obj = {
count: 0,
// ❌ 普通函数,this指向调用者
incrementOld: function() {
setTimeout(function() {
console.log(this.count) // undefined!
}, 1000)
},
// ✅ 箭头函数,this继承外层
incrementNew: function() {
setTimeout(() => {
console.log(this.count) // 0
}, 1000)
}
}

解构赋值

// 对象解构
const user = { name: 'Vue', version: 3, author: 'Evan' }
const { name, version } = user
console.log(name, version) // 'Vue' 3

// 重命名
const { name: framework } = user
console.log(framework) // 'Vue'

// 默认值
const { license = 'MIT' } = user
console.log(license) // 'MIT'

// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5]
console.log(first, second, rest) // 1 2 [3, 4, 5]

展开运算符

// 数组展开
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5]
console.log(arr2) // [1, 2, 3, 4, 5]

// 对象展开(浅拷贝)
const defaults = { theme: 'light', lang: 'zh' }
const settings = { ...defaults, theme: 'dark' }
console.log(settings) // { theme: 'dark', lang: 'zh' }

// 合并对象
const merged = { ...obj1, ...obj2 }

模板字符串

const name = 'Vue'
const version = 3

// ❌ 旧写法
const msg = 'Welcome to ' + name + ' ' + version

// ✅ 新写法
const msg = <code>Welcome to ${name} ${version}

// 多行字符串
const html = `

${name}

`

简写属性

const name = 'Vue'
const version = 3

// ❌ 旧写法
const app = {
name: name,
version: version,
sayHello: function() {
return 'Hello'
}
}

// ✅ 新写法
const app = {
name, // 等同于 name: name
version, // 等同于 version: version
sayHello() { // 方法简写
return 'Hello'
}
}

Promise和async/await

// Promise
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

// async/await(推荐)
async function getUsers() {
try {
const response = await fetch('/api/users')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}

// 并行请求
async function getAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
])
return { users, posts }
}

数组方法

const numbers = [1, 2, 3, 4, 5]

// map:转换每个元素
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]

// filter:筛选元素
const evens = numbers.filter(n => n % 2 === 0) // [2, 4]

// find:查找元素
const found = numbers.find(n => n > 3) // 4

// reduce:累积计算
const sum = numbers.reduce((acc, n) => acc + n, 0) // 15

// some/every:判断
const hasEven = numbers.some(n => n % 2 === 0) // true
const allPositive = numbers.every(n => n > 0) // true

// 链式调用
const result = numbers
.filter(n => n > 2)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0) // 120

可选链和空值合并

const user = {
profile: {
name: 'Vue'
// avatar 可能不存在
}
}

// ❌ 旧写法(容易报错)
const avatar = user && user.profile && user.profile.avatar

// ✅ 可选链
const avatar = user?.profile?.avatar // undefined,不会报错

// ❌ 旧写法
const name = user.name || 'Anonymous' // 空字符串也会用默认值

// ✅ 空值合并
const name = user.name ?? 'Anonymous' // 只有null/undefined才用默认值

Node.js和npm

什么是Node.js?

Node.js让JavaScript可以脱离浏览器运行。它主要用于:

  1. 运行构建工具(Vite、Webpack)
  2. 服务端开发(Express、Nest)
  3. 运行脚本

下载安装:https://nodejs.org/

npm基础命令

# 初始化项目
npm init -y

# 安装依赖
npm install vue # 安装到dependencies
npm install -D vite # 安装到devDependencies
npm install -g pnpm # 全局安装

# 版本管理
npm install vue@3 # 指定版本
npm install vue@latest # 最新版本

# 运行脚本
npm run dev # 开发服务器
npm run build # 构建生产版本

# 其他
npm update # 更新依赖
npm outdated # 检查过时依赖
npm audit # 安全检查

package.json

{
"name": "my-vue-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

pnpm:更快的替代品

# 安装pnpm
npm install -g pnpm

# 使用(命令基本相同)
pnpm install
pnpm add vue
pnpm dev

pnpm优点:

  • 更快(硬链接,节省磁盘空间)
  • 更严格(避免幽灵依赖)
  • 更安全(更好的依赖管理)

ES模块

import/export

// utils.js - 导出
export const PI = 3.14159

export function add(a, b) {
return a + b
}

export default class Calculator {
// ...
}

// main.js - 导入
import Calculator, { PI, add } from './utils.js'
import * as utils from './utils.js'

动态导入

// 懒加载
const module = await import('./heavy-module.js')

// Vue路由懒加载
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]

开发环境推荐

VSCode必备插件

  1. Vue - Official - Vue语法高亮和智能提示
  2. ESLint - 代码规范检查
  3. Prettier - 代码格式化
  4. Volar - Vue3专用(已集成到Vue - Official)

推荐配置

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

小结

你现在掌握了:

  • ES6+核心语法(箭头函数、解构、展开、async/await)
  • Node.js和npm基础操作
  • ES模块的import/export
  • 开发环境配置

下一期,我们将学习Vite - Vue3官方推荐的构建工具。它快得让你怀疑人生,配置简单到让你感动流泪。


练习任务

  1. 用解构赋值提取对象属性
  2. 用map/filter/reduce处理数组
  3. 用async/await改写Promise链式调用
  4. 创建一个npm项目,安装vue依赖

下期预告:《Vite快速入门:秒级启动的开发体验》—— 为什么Vite这么快?如何配置Vite项目?

Views: 13