Vue3核心语法回顾与Composition深入

Vue3核心语法回顾与Composition深入 -- pd的前端笔记

文章目录

    • [Vue3核心语法回顾与Composition深入 -- pd的前端笔记](#Vue3核心语法回顾与Composition深入 -- pd的前端笔记)
  • [第一讲:Vue 3 核心语法回顾与 Composition API 深入](#第一讲:Vue 3 核心语法回顾与 Composition API 深入)
    • [1.1 从零创建 Vue 3 项目 + 认识 `<script setup>` ------ 你的第一个响应式计数器](#1.1 从零创建 Vue 3 项目 + 认识 <script setup> —— 你的第一个响应式计数器)
    • [1.2 深入 ref 与 reactive ------ 响应式数据的两种写法怎么选?](#1.2 深入 ref 与 reactive —— 响应式数据的两种写法怎么选?)
    • [1.3 计算属性、监听器与生命周期 ------ 让组件"活"起来](#1.3 计算属性、监听器与生命周期 —— 让组件“活”起来)
    • [1.4 自定义 Composables ------ 把逻辑变成"乐高积木"](#1.4 自定义 Composables —— 把逻辑变成“乐高积木”)
      • [改造1.1 篇的计数器逻](#改造1.1 篇的计数器逻)
      • [第二个 Composable:useLocalStorage](#第二个 Composable:useLocalStorage)
      • [第三个 Composable:useFetch(引入 fetch 和 AbortController)](#第三个 Composable:useFetch(引入 fetch 和 AbortController))
      • [Composable 设计最佳实践(TS + 工程化)](#Composable 设计最佳实践(TS + 工程化))

写在最前: 环境配置

shell 复制代码
# 因为latest要求nodejs > 20.19, 但是我的nodejs版本是20.10
# 所以需要安装nvm 进行node版本管理
# 管理员权限
nvm install 22.12.0
nvm use 22.12.0

npm create vue@latest my-vue3-app
cd my-vue3-app
npm install
npm run dev

第一讲:Vue 3 核心语法回顾与 Composition API 深入

1.1 从零创建 Vue 3 项目 + 认识 <script setup> ------ 你的第一个响应式计数器

替换 src/App.vue 的全部内容为以下代码:

html 复制代码
<!-- src/App.vue -->
<script setup lang="ts">
// 👆 注意:加上 lang="ts",告诉 Vue 这是 TypeScript 代码!

import { ref } from 'vue'

// 使用 ref 创建一个响应式变量,初始值为数字 0
// TypeScript 会自动推断 count 的类型为 Ref<number>
const count = ref(0)

// 定义 increment 函数:没有参数,没有返回值(void)
function increment(): void {
  // 在 TypeScript 中,ref 的值必须通过 .value 访问
  count.value = count.value + 1
}

// 定义 reset 函数
function reset(): void {
  count.value = 0
}
</script>

<template>
  <div style="padding: 20px; font-family: Arial, sans-serif">
    <h1>我的第一个 Vue 3 + TypeScript 计数器</h1>
    <p>当前数字是:{{ count }}</p>
    <button @click="increment">点我 +1</button>
    <button @click="reset" style="margin-left: 10px">重置</button>
  </div>
</template>

其中:

  • Ref<T> 是 Vue 提供的泛型接口,表示一个响应式引用
  • T 是你传入的值的类型(这里是 number)

模板中为什么不用 .value?

  • <template> 中,所有 Ref<T> 类型的变量都会自动 .value 解包
  • 但在 <script setup> 的 JS/TS 逻辑中,必须手动写 .value,一旦不加 .value就会从 Ref<number> 变成 number,失去响应性!

升级组件:让用户自定义每次点击加多少

html 复制代码
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
// 新增:步长,默认为 1,类型是 number
const step = ref(1)

function increment(): void {
  // 使用 step.value 作为增量
  count.value += step.value
}

function reset(): void {
  count.value = 0
}

// 处理输入框变化
function handleStepChange(event: Event): void {
  const target = event.target as HTMLInputElement
  // 将字符串转为数字,NaN 时默认为 1
  const value = Number(target.value)
  step.value = isNaN(value) ? 1 : value
}
</script>

<template>
  <div style="padding: 20px; font-family: Arial, sans-serif">
    <h1>增强版计数器(带步长)</h1>
    <p>当前数字:{{ count }}</p>
    
    <div style="margin: 10px 0">
      <label>步长: </label>
      <!-- 输入框绑定 change 事件 -->
      <input 
        type="number" 
        :value="step" 
        @change="handleStepChange"
        style="width: 80px; padding: 4px"
      />
    </div>

    <button @click="increment">点我 +{{ step }}</button>
    <button @click="reset" style="margin-left: 10px">重置</button>
  </div>
</template>

🔍 关键 TS 点解析:

  • event: Event:明确事件类型
  • event.target as HTMLInputElement:类型断言,告诉 TS "我知道 target 是 input 元素"
  • Number() 转换后用 isNaN() 校验,避免 step.value = NaN

💡 如果你不写类型断言,TS 会报错:"Property 'value' does not exist on type 'EventTarget'",因为 event.target 默认是 EventTarget | null,不一定是 input。

TypeScript 如何帮助你避免常见 Bug?

假设你误写了:

js 复制代码
function badIncrement() {
  count.value = count.value + "1" // 字符串拼接!
}

TS 会立刻报错:Operator '+' cannot be applied to types 'number' and 'string'.

而在纯 JS 中,这会导致 count 变成 "01", "011", ... ------ 一个隐蔽的 bug!

✅ 本篇小结(TS + Vue 3 初学者重点)

概念 说明
lang="ts" 必须加在 <script setup> 上才能用 TS
ref(0) 返回 Ref<number>,JS/TS 中读写需 .value
模板自动解包 {``{ count }} 不用 .value
函数类型标注 推荐写 : void: ReturnType
类型断言 event.target as HTMLInputElement 获取具体 DOM 类型
类型安全 防止运行时错误,提升代码健壮性

1.2 深入 ref 与 reactive ------ 响应式数据的两种写法怎么选?

✅ 写法一:用 ref 包装整个对象(推荐!)

html 复制代码
<!-- UserCardRef.vue -->
<script setup lang="ts">
import { ref } from 'vue'

// 定义接口,描述用户结构
interface User {
  name: string
  age: number
}

// 使用 ref 包裹整个对象
const user = ref<User>({
  name: '张三',
  age: 25
})

// 更新函数
function updateName(newName: string): void {
  user.value.name = newName // ✅ 安全!响应性保留
}

function updateAge(newAge: number): void {
  user.value.age = newAge
}
</script>

<template>
  <div style="border: 1px solid #ccc; padding: 16px; margin: 10px">
    <h3>使用 ref 的用户卡片</h3>
    <p>姓名:{{ user.name }}</p>
    <p>年龄:{{ user.age }}</p>
    <button @click="updateName('李四')">改名李四</button>
    <button @click="updateAge(30)" style="margin-left: 8px">年龄+5</button>
  </div>
</template>

❌ 写法二:用 reactive + 直接解构(危险!)

html 复制代码
<!-- UserCardReactiveBad.vue -->
<script setup lang="ts">
import { reactive } from 'vue'

interface User {
  name: string
  age: number
}

// 使用 reactive 创建响应式对象
const user = reactive<User>({
  name: '张三',
  age: 25
})

// ⚠️ 危险操作:直接解构!
const { name, age } = user

function updateName(newName: string): void {
  user.name = newName // ✅ 这个还能更新
}

// 但模板里如果用 {{ name }},就永远不会变了!
</script>

<template>
  <div style="border: 1px solid #f99; padding: 16px; margin: 10px; background: #ffecec">
    <h3 style="color: red">错误示范:reactive + 直接解构</h3>
    <!-- ⚠️ 这里的 name 是解构时的快照,不是响应式的! -->
    <p>姓名:{{ name }}</p> <!-- ❌ 不会更新! -->
    <p>年龄:{{ user.age }}</p> <!-- ✅ 这个会更新 -->
    <button @click="updateName('王五')">改名王五</button>
  </div>
</template>

🔴 问题:

点击按钮后,user.name 确实变了,但模板中的 {{ name }} 不会更新!

因为 const { name } = user 只是把 user.name 的当前值赋给 name,它是个普通字符串,没有响应性。

💡 这是新手最容易踩的坑之一!

ref vs reactive:到底怎么选?(TS 最佳实践)

场景 推荐方案 理由
简单值(数字、字符串、布尔) ref 直观,无需包装对象
复杂对象(用户、表单、配置) ref<{ ... }> 避免 reactive 解构陷阱,类型更清晰
需要解构到模板 refreactive + toRefs ref 更简单;toRefs 适合已有 reactive 代码
性能敏感场景 两者差异极小,优先可读性 Vue 3 内部优化得很好

创建一个"表单状态管理器"

html 复制代码
<!-- LoginForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface LoginForm {
  email: string
  password: string
  remember: boolean
}

// 初始化表单状态
const form = ref<LoginForm>({
  email: '',
  password: '',
  remember: false
})

// 更新字段的通用函数
function updateField<K extends keyof LoginForm>(
  field: K,
  value: LoginForm[K]
): void {
  form.value[field] = value
}

// 提交表单
function onSubmit(): void {
  console.log('提交表单:', form.value)
  alert('表单已提交!请打开控制台查看数据')
}
</script>

<template>
  <div style="max-width: 400px; margin: 20px auto; padding: 20px; border: 1px solid #ddd">
    <h2>登录表单(ref 管理)</h2>
    <div style="margin: 10px 0">
      <label>Email:</label>
      <input 
        v-model="form.email" 
        type="email" 
        style="width: 100%; padding: 6px"
      />
    </div>
    <div style="margin: 10px 0">
      <label>Password:</label>
      <input 
        v-model="form.password" 
        type="password" 
        style="width: 100%; padding: 6px"
      />
    </div>
    <div style="margin: 10px 0">
      <label>
        <input 
          v-model="form.remember" 
          type="checkbox" 
        /> 记住我
      </label>
    </div>
    <button @click="onSubmit" style="padding: 8px 16px; background: #42b883; color: white; border: none">
      登录
    </button>
    
    <!-- 实时预览表单数据 -->
    <pre style="margin-top: 16px; background: #f5f5f5; padding: 10px">{{ form }}</pre>
  </div>
</template>

🔍 TS 亮点解析:

  • ref<LoginForm>:明确类型,编辑器自动补全 email/password
  • K extends keyof LoginForm:泛型约束,确保 field 是合法 key
  • v-model="form.email":Vue 自动处理 .value,你不用写 form.value.email
    💡 这就是现代 Vue 3 + TS 开发的典型模式:类型安全 + 响应式 + 简洁模板
概念 正确做法 错误做法
响应式对象 const obj = ref({ ... }) const { x } = reactive({ x: 1 })
修改值 obj.value.prop = newValue obj = { ... }(覆盖 ref)
模板绑定 {``{ obj.prop }} {``{ prop }}(未用 toRefs)
类型标注 ref<MyType>(initial) 不写类型(失去 TS 优势)

1.3 计算属性、监听器与生命周期 ------ 让组件"活"起来

🎯 场景:用户全名 = 名 + 姓

我们有一个用户对象,包含 firstName 和 lastName,想自动合成 fullName。

html 复制代码
<!-- FullNameDemo.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'

// 原始数据
const firstName = ref('张')
const lastName = ref('三')

// ✅ 计算属性:自动派生 fullName
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 更新函数
function changeName(): void {
  firstName.value = '李'
  lastName.value = '四'
}
</script>

<template>
  <div style="padding: 20px; font-family: Arial">
    <h2>计算属性示例:全名合成</h2>
    <p>名:{{ firstName }}</p>
    <p>姓:{{ lastName }}</p>
    <p style="font-weight: bold; color: #42b883">全名:{{ fullName }}</p>
    <button @click="changeName">切换为 李四</button>
  </div>
</template>

🔍 关键点解析:

  • computed(() => {...}) 返回一个 只读的 Ref
  • 模板中直接写 {{ fullName }}(自动解包)
  • 当 firstName 或 lastName 变化时,fullName 自动重新计算
  • TS 自动推断 fullName 类型为 ComputedRef

⚠️ 不要手动调用 fullName() ------ 它不是函数,是响应式引用!

监听器(watch):当数据变化时,我想做点事

🎯 场景:监听搜索关键词,发起 API 请求

html 复制代码
<!-- WatchDemo.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue'

const keyword = ref('')
const searchResult = ref<string[]>([])

// ✅ 监听 keyword 变化
watch(
  keyword, // 要监听的源(可以是 ref、getter 函数等)
  (newVal: string, oldVal: string) => {
    console.log(`关键词从 "${oldVal}" 变为 "${newVal}"`)
    if (newVal.trim()) {
      // 模拟搜索
      searchResult.value = [`结果1 for ${newVal}`, `结果2 for ${newVal}+10086`]
    } else {
      searchResult.value = []
    }
  },
  {
    immediate: false, // 是否立即执行一次?默认 false
    deep: false       // 是否深度监听?对对象/数组才需要
  }
)

function clearSearch(): void {
  keyword.value = ''
}
</script>

<template>
  <div style="padding: 20px; max-width: 500px">
    <h2>监听器示例:搜索关键词</h2>
    <input 
      v-model="keyword" 
      placeholder="输入关键词..." 
      style="width: 100%; padding: 8px; margin: 8px 0"
    />
    <button @click="clearSearch">清空</button>
    
    <ul style="margin-top: 16px">
      <li v-for="item in searchResult" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>

🔍 watch 参数详解(TS 类型安全):

参数 说明
source 要监听的数据: - ref → 直接传 keyword - 多个 → [ref1, ref2] - 表达式 → () => someRef.value.id
callback (newValue, oldValue) => void TS 会自动推断 newValue 类型
options { immediate?: boolean, deep?: boolean }

✅ 最佳实践:

  • 简单值监听 → 用 ref
  • 复杂表达式监听 → 用 getter 函数:watch(() => user.value?.profile?.name, ...)

🆚 watch vs watchEffect:怎么选?

对比项 watch(source, callback) watchEffect(() => { ... })
触发时机 仅当 source 变化时 组件 setup 时立即执行 + 依赖变化时
能访问 oldValue ✅ 是 ❌ 否
适合场景 需要新旧值对比、条件执行 自动追踪依赖(如日志、简单副作用)

✅ watchEffect 示例:自动打印当前计数

js 复制代码
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  // 自动追踪 count.value
  console.log('当前计数:', count.value)
})

// 每次 count 变化,都会 log
// 只要watcheffect中的依赖项变化时就会执行

生命周期钩子:在正确时机执行副作用

Vue 3 将生命周期函数以 onXxx 形式导出,全部在 setup() 中调用。

🔁 完整生命周期顺序(简化版):

text 复制代码
onBeforeMount → onMounted → (用户交互) → onUnmounted
html 复制代码
<script setup lang="ts">
import { ref, onMounted, onUnmounted, onBeforeMount } from 'vue'

const message = ref('等待挂载...')

// 组件挂载前
onBeforeMount(() => {
  console.log('【onBeforeMount】DOM 还没生成')
})

// ✅ 组件挂载后(最常用!)
onMounted(() => {
  console.log('【onMounted】DOM 已生成,可以操作 DOM 或发起请求')
  message.value = '组件已挂载!'
  
  // 模拟获取数据
  setTimeout(() => {
    message.value = '数据加载完成!'
  }, 1000)
})

// 组件卸载前(清理定时器、事件监听等)
onUnmounted(() => {
  console.log('【onUnmounted】组件即将销毁,记得清理!')
})
</script>

✅ 关键规则:

  • 不要在 setup 顶层直接写副作用(如 fetchData()),因为此时 DOM 未就绪
  • 所有副作用(请求、订阅、DOM 操作)必须放在 onMounted 或之后
动手实验:构建一个"实时字数统计器"

结合 ref + computed + watch + onMounted:

html 复制代码
<!-- WordCounter.vue -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

const text = ref('')
const maxLength = ref(100)

// ✅ 计算属性:当前字数 & 剩余字数
const wordCount = computed(() => text.value.length)
const remaining = computed(() => maxLength.value - wordCount.value)
const isOverLimit = computed(() => wordCount.value > maxLength.value)

// ✅ 监听 text 变化,保存到 localStorage
watch(text, (newText) => {
  localStorage.setItem('draft', newText)
})

// ✅ 挂载时恢复草稿
onMounted(() => {
  const saved = localStorage.getItem('draft')
  if (saved) {
    text.value = saved
  }
})

function clearDraft(): void {
  text.value = ''
  localStorage.removeItem('draft')
}
</script>

<template>
  <div style="max-width: 600px; margin: 20px auto; padding: 20px; font-family: Arial">
    <h2>实时字数统计器(带本地存储)</h2>
    
    <textarea 
      v-model="text" 
      placeholder="开始输入..."
      style="width: 100%; height: 120px; padding: 8px; margin: 10px 0"
    ></textarea>
    
    <div :style="{ color: isOverLimit ? 'red' : 'green' }">
      {{ wordCount }} / {{ maxLength }} 字
      <span v-if="isOverLimit" style="color: red; margin-left: 8px">⚠️ 超出限制!</span>
    </div>
    
    <button @click="clearDraft" style="margin-top: 10px">清空草稿</button>
  </div>
</template>

🔍 技术点总结:

  • computed:派生 wordCountremainingisOverLimit
  • watch:监听 text 并持久化到 localStorage
  • onMounted:恢复上次编辑内容

✅ 本篇小结:何时用什么?

需求 推荐 API 理由
派生状态(如格式化、过滤) computed 缓存、高效、自动依赖追踪
数据变化时执行副作用(需新旧值) watch 精确控制、可选 immediate/deep
自动追踪依赖的简单副作用 watchEffect 代码更简洁
操作 DOM / 发起请求 onMounted 确保 DOM 已就绪
清理资源(定时器、监听) onUnmounted 防止内存泄漏

1.4 自定义 Composables ------ 把逻辑变成"乐高积木"

在 Vue 3 中,Composable(组合式函数) 是以 useXxx 命名的函数,用于封装可复用的逻辑。

💡 类比:就像乐高积木------你把"计数逻辑"、"本地存储逻辑"、"API 请求逻辑"分别做成标准积木,以后搭任何组件都能直接拼。

✅ 优势:

  • 逻辑复用:不用重复写相同代码
  • 关注点分离:组件只负责 UI,逻辑抽离到 useXxx
  • 类型安全:配合 TS,输入输出清晰
  • 易于测试:纯函数,不依赖组件上下文

改造1.1 篇的计数器逻

我们把第 1.1 篇的计数器逻辑抽出来:

ts 复制代码
// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue: number = 0) {
  const count = ref(initialValue)
  
  const isEven = computed(() => count.value % 2 === 0)
  
  function increment(step: number = 1) {
    count.value += step
  }
  
  function reset() {
    count.value = initialValue
  }
  
  return {
    count,
    isEven,
    increment,
    reset
  }
}

🔍 TS 说明:

  • 参数 initialValue: number = 0:带默认值的数字参数
  • 返回对象的类型由 TS 自动推断,无需手动标注(但你也可以用接口显式声明)

在组件中使用:

html 复制代码
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'

const { count, isEven, increment, reset } = useCounter(10)
</script>

<template>
  <div style="padding: 20px">
    <p>计数:{{ count }}({{ isEven ? '偶数' : '奇数' }})</p>
    <button @click="increment(1)">+1</button>
    <button @click="increment(5)" style="margin: 0 8px">+5</button>
    <button @click="reset">重置</button>
  </div>
</template>

✅ 完美复用!而且逻辑和 UI 彻底解耦。

第二个 Composable:useLocalStorage

localStorage 是什么?

它是浏览器提供的持久化存储机制,数据以字符串形式保存在用户设备上,关闭浏览器后依然存在(除非手动清除)。

典型用途:保存用户偏好、表单草稿、主题设置等。

注意:

只能存字符串 → 存对象需 JSON.stringify,取时 JSON.parse

同源策略限制(不同域名不能共享)

容量约 5~10MB(因浏览器而异)

现在,我们封装一个能自动同步 ref 到 localStorage 的 Composable:

ts 复制代码
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

/**
 * 创建一个与 localStorage 同步的响应式引用
 * @param key localStorage 的键名
 * @param defaultValue 初始值(如果 localStorage 中没有)
 */
export function useLocalStorage<T>(key: string, defaultValue: T) {
  // 尝试从 localStorage 读取
  const storedValue = localStorage.getItem(key)
  let initialValue: T
  
  try {
    // 如果有存储值,解析为 JSON;否则用默认值
    initialValue = storedValue ? JSON.parse(storedValue) : defaultValue
  } catch (error) {
    // 解析失败(比如格式损坏),回退到默认值
    console.warn(`Failed to parse localStorage item "${key}"`, error)
    initialValue = defaultValue
  }

  const value = ref<T>(initialValue)

  // 监听变化,自动存入 localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true }) // deep: true 支持对象/数组

  return value
}

🔍 TS 亮点:

  • 泛型 <T>:让函数适用于任意类型(string / number / object)
  • deep: true:确保嵌套对象变化也能触发存储
  • 错误处理:防止 JSON.parse 崩溃

在组件中使用:

html 复制代码
<!-- ThemeSwitcher.vue -->
<script setup lang="ts">
import { useLocalStorage } from '@/composables/useLocalStorage'

// 自动从 localStorage 读取 'theme',默认 'light'
const theme = useLocalStorage<'light' | 'dark'>('theme', 'light')

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
  // 自动保存到 localStorage!
}
</script>

<template>
  <div :style="{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#000', padding: '20px' }">
    <h2>主题切换器(自动持久化)</h2>
    <p>当前主题:{{ theme }}</p>
    <button @click="toggleTheme">切换主题</button>
    <p>刷新页面试试,主题会保留!</p>
  </div>
</template>

第三个 Composable:useFetch(引入 fetch 和 AbortController)

🔎 先认识 fetch(现代 Web API)

fetch 是什么?

它是浏览器原生提供的网络请求 API,用于从服务器获取或发送数据(替代老旧的 XMLHttpRequest)。

基本用法:fetch(url).then(res => res.json())

优点:基于 Promise,支持 async/await,内置流处理

注意:

默认不带 cookie → 需加 { credentials: 'include' }

HTTP 错误(如 404)不会 reject Promise → 需手动检查 res.ok

🔎 再认识 AbortController(取消请求)

AbortController 是什么?

它是浏览器提供的请求取消机制。当你发起一个 fetch,可以传入 signal,后续调用 abort() 即可中断请求。

典型场景:组件卸载时取消未完成的请求,避免"内存泄漏"或状态更新错误。

现在,封装一个带 loading、error、自动取消的 useFetch:

ts 复制代码
// composables/useFetch.ts (升级版)
import { ref, watch, onUnmounted, Ref } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  loading: Ref<boolean>
}

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

  // 创建一个可取消的请求控制器
  let controller: AbortController

  const execute = async (currentUrl: string) => {
    // 取消上一次请求(如果存在)
    if (controller) {
      controller.abort()
    }

    controller = new AbortController()
    loading.value = true
    error.value = null

    try {
      const response = await fetch(currentUrl, {
        signal: controller.signal
      })

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

      const result = await response.json()
      data.value = result
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        error.value = err as Error
      }
    } finally {
      loading.value = false
    }
  }

  // 核心:监听 url 变化
  if (typeof url === 'string') {
    // 静态 URL
    execute(url)
  } else {
    // 动态 URL(ref)
    watch(url, (newUrl) => {
      if (newUrl) execute(newUrl)
    }, { immediate: true })
  }

  // 组件卸载时取消请求
  onUnmounted(() => {
    if (controller) {
      controller.abort()
    }
  })

  return { data, error, loading }
}

在组件中使用:

html 复制代码
<!-- UserFetcher.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFetch } from '@/composables/useFetch'

const id = ref<number>(1)

// 动态生成 URL
const url = computed(() => `https://jsonplaceholder.typicode.com/users/${id.value}`)

// 传入 ref<string>,useFetch 会自动监听变化
const { data, error, loading } = useFetch<{ id: number; name: string }>(url)
</script>

<template>
  <div style="padding: 20px; max-width: 500px">
    <h2>用户信息加载器(修复版)</h2>
    <input v-model.number="id" type="number" placeholder="请输入用户ID" />
    
    <div v-if="loading">正在加载...</div>
    <div v-else-if="error" style="color: red">加载失败:{{ error.message }}</div>
    <div v-else-if="data">
      <p>ID: {{ data.id }}</p>
      <p>姓名: {{ data.name }}</p>
    </div>
  </div>
</template>

Composable 设计最佳实践(TS + 工程化)

原则 说明
命名规范 一律 useXxx,一眼识别是 Composable
单一职责 一个 Composable 只做一件事(如 useLocalStorage 不处理 UI)
返回 refs 返回 ref 而非普通值,确保模板能直接用
泛型支持 <T> 让函数适用于多种类型
副作用清理 onUnmounted 或返回 cleanup 函数
避免直接操作 DOM 保持逻辑与渲染解耦

✅ 本篇小结

Composable 功能 关键技术
useCounter 计数逻辑复用 ref + computed
useLocalStorage 持久化状态 localStorage + watch + JSON
useFetch 安全 API 请求 fetch + AbortController + onUnmounted
相关推荐
code袁10 小时前
基于Springboot+Vue的家教小程序的设计与实现
vue.js·spring boot·小程序·vue·家教小程序
colicode13 小时前
Objective-C语音验证码接口API示例代码:老版iOS应用接入语音验证教程
前端·c++·ios·前端框架·objective-c
国产化创客15 小时前
ESP32平台嵌入式Web前端框架选型分析
前端·物联网·前端框架·智能家居
是梦终空1 天前
计算机毕业设计266—基于Springboot+Vue3的共享单车管理系统(源代码+数据库)
数据库·spring boot·vue·课程设计·计算机毕业设计·源代码·共享单车系统
前端达人1 天前
都2026年了,还在用Options API?Vue组合式API才是你该掌握的“正确姿势“
前端·javascript·vue.js·前端框架·ecmascript
colicode1 天前
C#语音验证码API示例代码:快速实现.NET环境下的语音验证调用逻辑
前端·前端框架·语音识别
是梦终空1 天前
计算机毕业设计267—基于Springboot+vue3+小程序的医院挂号系统(源代码+数据库)
spring boot·小程序·vue·毕业设计·课程设计·医院挂号系统·源代码
colicode2 天前
C++语音验证码接口API示例代码详解:高性能C++语音校验接入Demo
前端·c++·前端框架·语音识别
无巧不成书02182 天前
React Native 鸿蒙开发(RNOH)深度适配
前端·javascript·react native·react.js·前端框架·harmonyos