Vue 2 与 Vue 3 接口请求完全指南:数据请求应该写在哪里?

前言

在前几篇博客中,我们聊了 Vue 的响应式 API(ref/reactive)、侦听器(watch)和计算属性(computed)。今天,我们把目光投向每个 Vue 项目都绕不开的核心问题——接口请求应该写在哪里?

当你打开一个 Vue 项目,几乎每个页面组件都需要在初始化时从后端获取数据。那么问题来了:

  • 请求应该写在 created 还是 mounted
  • Vue 3 中 setuponMounted 怎么选?
  • 初始化数据的方法应该如何封装?
  • 父子组件的数据请求顺序如何控制?

本文将系统梳理 Vue 2 和 Vue 3 中发起接口请求的最佳实践,通过大量代码案例和真实项目场景,帮你彻底搞清楚"请求应该写在哪"这个问题。

一、Vue 2 中的接口请求:created vs mounted

1.1 Vue 2 生命周期回顾

在 Vue 2 中,与数据请求最相关的生命周期钩子有两个:createdmounted

生命周期 执行时机 data 可访问 DOM 可访问 适用场景
created 实例创建完成后 ✅ 可以 ❌ 不可以 纯数据获取、不依赖 DOM 的初始化
mounted DOM 挂载完成后 ✅ 可以 ✅ 可以 依赖 DOM 的操作、第三方库初始化

关键在于created 在 DOM 挂载之前执行,mounted 在 DOM 挂载之后执行。

1.2 黄金法则:优先在 created 中请求数据

绝大多数情况下,将数据请求放在 created 钩子中是最佳实践:

export default {
  data() {
    return {
      userList: [],
      loading: false,
      error: null
    }
  },
  // ✅ 推荐:在 created 中发起请求
  created() {
    this.fetchUserList()
  },
  methods: {
    async fetchUserList() {
      this.loading = true
      this.error = null
      try {
        const res = await axios.get('/api/users')
        this.userList = res.data
      } catch (err) {
        this.error = err.message
      } finally {
        this.loading = false
      }
    }
  }
}

为什么优先用 created

  1. 更早发起请求,减少白屏时间created 在组件初始化后立即执行,远早于 DOM 的挂载。请求和 DOM 渲染可以并行进行,用户等待时间更短
  2. 逻辑解耦:数据获取是组件的内部逻辑,通常不依赖 DOM,放在 created 中符合"关注点分离"原则
  3. SSR 友好:在服务端渲染(SSR)场景中,mounted 在服务端不会执行,而 created 在服务端和客户端都会执行

1.3 什么时候应该用 mounted?

虽然 created 是首选,但在以下场景中,必须或应该使用 mounted

场景一:需要操作 DOM

export default {
  data() {
    return {
      chartData: null
    }
  },
  // ✅ 需要操作 DOM,用 mounted
  mounted() {
    // 获取图表容器尺寸
    const container = this.$refs.chartContainer
    const width = container.offsetWidth
    
    // 根据容器尺寸加载图表数据
    this.fetchChartData(width)
  },
  methods: {
    async fetchChartData(width) {
      const res = await axios.get('/api/chart', { params: { width } })
      this.chartData = res.data
      // 初始化图表库
      this.initChart(this.$refs.chartContainer, this.chartData)
    }
  }
}

场景二:依赖第三方库初始化

export default {
  mounted() {
    // 地图 SDK 需要 DOM 容器
    this.initMap()
    // 地图初始化完成后加载数据
    this.fetchMapData()
  }
}

场景三:父子组件数据依赖(重要!)

组件的加载顺序是:父组件先执行前三个生命周期(beforeCreatecreatedbeforeMount),然后执行子组件的生命周期,子组件全部挂载完毕后,父组件才执行 mounted

如果父组件需要等待子组件的数据(即子组件的数据优先于父组件),那么父组件的请求应该放在 mounted 中:

// 父组件
export default {
  components: { ChildComponent },
  data() {
    return { parentData: null }
  },
  // ✅ 父组件需要等待子组件数据,放在 mounted
  mounted() {
    // 此时子组件已经挂载完成,可以安全获取子组件的数据
    this.parentData = this.$refs.child.getData()
  }
}

1.4 为什么不放在 beforeCreate 中?

beforeCreate 阶段,data 和 methods 都还未初始化,无法访问 this.datathis.methods

export default {
  // ❌ 错误:beforeCreate 中无法调用 methods
  beforeCreate() {
    // this.fetchUserList 是 undefined,会报错
    this.fetchUserList()
  },
  methods: {
    fetchUserList() { /* ... */ }
  }
}

但如果请求逻辑不依赖 methods,直接在 beforeCreate 中写也是可以的,只是不推荐,因为代码组织不够清晰。

二、Vue 3 中的接口请求:setup vs onMounted

2.1 Vue 3 生命周期变化

Vue 3 将生命周期钩子统一为组合式 API 函数,需要从 vue 中导入使用:

Vue 2 Vue 3 组合式 API 说明
created setup() 中直接写 setup 替代了 created
mounted onMounted() 需要导入
beforeDestroy onBeforeUnmount() 名称变更
destroyed onUnmounted() 名称变更

核心变化created 的生命周期逻辑直接写在 setup()<script setup> 的顶层代码中,不需要额外的钩子函数。

2.2 在 setup 中直接发起请求

在 Vue 3 的组合式 API 中,setup() 函数本身就在 created 之前执行。因此,created 阶段的逻辑直接写在 setup 顶层即可。

使用 <script setup> 语法糖

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

// ====== 响应式数据 ======
const userList = ref([])
const loading = ref(false)
const error = ref(null)

// ====== 请求方法 ======
const fetchUserList = async () => {
  loading.value = true
  error.value = null
  try {
    const res = await axios.get('/api/users')
    userList.value = res.data
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

// ====== ✅ 直接调用:相当于 Vue 2 的 created ======
// 在 <script setup> 顶层直接调用,组件实例创建时立即执行
fetchUserList()
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误:{{ error }}</div>
  <ul v-else>
    <li v-for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

关键理解<script setup> 中的顶层代码在组件实例创建时执行,相当于 Vue 2 中 created 钩子的位置。因此,直接在顶层调用请求函数就相当于在 created 中发起请求。

2.3 什么时候用 onMounted?

与 Vue 2 类似,当需要操作 DOM依赖 DOM 渲染完成时,使用 onMounted

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

const chartData = ref(null)
const chartContainer = ref(null)

// ✅ 需要操作 DOM,用 onMounted
onMounted(async () => {
  // 获取容器尺寸
  const width = chartContainer.value?.offsetWidth || 800
  
  // 根据尺寸加载数据
  const res = await axios.get('/api/chart', { params: { width } })
  chartData.value = res.data
  
  // 初始化图表库(需要 DOM 容器)
  initChart(chartContainer.value, chartData.value)
})

const initChart = (container, data) => {
  // 第三方图表库初始化逻辑
}
</script>

<template>
  <div ref="chartContainer" style="width:100%;height:400px;"></div>
</template>

2.4 setup 顶层 vs onMounted 的执行顺序

在 Vue 3 中,setup 中的顶层代码永远先于 onMounted 执行,与书写顺序无关

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

console.log('1. setup 顶层代码')

onMounted(() => {
  console.log('3. onMounted 执行')
})

console.log('2. setup 顶层代码(即使写在 onMounted 后面,也先执行)')
</script>
// 输出顺序:1 → 2 → 3

三、初始化数据方法的设计模式

3.1 基础模式:直接调用

最简单的做法:在 created / setup 中直接调用请求方法。

Vue 2

export default {
  data() {
    return { posts: [], loading: false }
  },
  created() {
    this.fetchPosts()
  },
  methods: {
    async fetchPosts() {
      this.loading = true
      try {
        const res = await axios.get('/api/posts')
        this.posts = res.data
      } finally {
        this.loading = false
      }
    }
  }
}

Vue 3

<script setup>
const posts = ref([])
const loading = ref(false)

const fetchPosts = async () => {
  loading.value = true
  try {
    const res = await axios.get('/api/posts')
    posts.value = res.data
  } finally {
    loading.value = false
  }
}

// 直接调用
fetchPosts()
</script>

3.2 进阶模式:封装 useFetch 组合式函数

在 Vue 3 中,推荐将数据请求封装成可复用的组合式函数(Composables)

// composables/useFetch.js
import { ref, reactive } from 'vue'
import axios from 'axios'

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

  const fetchData = async (params = {}) => {
    loading.value = true
    error.value = null
    try {
      const res = await axios.get(url, { ...options, params })
      data.value = res.data
      return res.data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  // 立即执行一次
  const execute = fetchData

  return { data, loading, error, fetchData, execute }
}

在组件中使用

<script setup>
import { useFetch } from '@/composables/useFetch'

// 一行代码搞定请求 + 状态管理
const { data: userList, loading, error, execute: refreshUsers } = useFetch('/api/users')

// 组件初始化时自动发起请求(useFetch 内部已调用)
// 也可以手动刷新
const handleRefresh = () => {
  refreshUsers()
}
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误:{{ error }}</div>
  <ul v-else>
    <li v-for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
  <button @click="handleRefresh">刷新</button>
</template>

3.3 高级模式:带依赖刷新的 useFetch

在实际项目中,经常需要根据某个参数变化重新请求数据

// composables/useFetchWithDeps.js
import { ref, watch, toValue } from 'vue'
import axios from 'axios'

export function useFetchWithDeps(url, deps = [], options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      // 将 deps 作为请求参数
      const params = deps.map(d => toValue(d))
      const res = await axios.get(url, { ...options, params: { deps: params } })
      data.value = res.data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  // 初始执行
  fetchData()

  // 监听依赖变化,自动重新请求
  if (deps.length > 0) {
    watch(deps, () => {
      fetchData()
    }, { deep: true })
  }

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

在组件中使用

<script setup>
import { ref } from 'vue'
import { useFetchWithDeps } from '@/composables/useFetchWithDeps'

const userId = ref(1)
const userRole = ref('admin')

// userId 或 userRole 变化时自动重新请求
const { data: userInfo, loading } = useFetchWithDeps(
  '/api/user',
  [userId, userRole]
)
</script>

四、真实项目实战

4.1 场景一:用户列表页面(纯数据请求)

Vue 3 + <script setup> 实现

<template>
  <div class="user-page">
    <!-- 搜索栏 -->
    <div class="search-bar">
      <input v-model="searchKeyword" placeholder="搜索用户" />
      <button @click="handleSearch">搜索</button>
    </div>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误状态 -->
    <div v-else-if="error" class="error">{{ error }}</div>
    
    <!-- 用户列表 -->
    <div v-else>
      <div v-for="user in userList" :key="user.id" class="user-item">
        <img :src="user.avatar" :alt="user.name" />
        <span>{{ user.name }}</span>
        <span>{{ user.email }}</span>
      </div>
      
      <!-- 分页 -->
      <div class="pagination">
        <button @click="prevPage" :disabled="page === 1">上一页</button>
        <span>第 {{ page }} / {{ totalPages }} 页</span>
        <button @click="nextPage" :disabled="page === totalPages">下一页</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import axios from 'axios'

// ====== 状态定义 ======
const userList = ref([])
const loading = ref(false)
const error = ref(null)
const searchKeyword = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)

// ====== 计算属性 ======
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

// ====== 请求方法 ======
const fetchUserList = async () => {
  loading.value = true
  error.value = null
  try {
    const res = await axios.get('/api/users', {
      params: {
        keyword: searchKeyword.value,
        page: page.value,
        pageSize: pageSize.value
      }
    })
    userList.value = res.data.list
    total.value = res.data.total
  } catch (err) {
    error.value = err.message || '加载用户列表失败'
  } finally {
    loading.value = false
  }
}

// ====== 操作方法 ======
const handleSearch = () => {
  page.value = 1
  fetchUserList()
}

const prevPage = () => {
  if (page.value > 1) {
    page.value--
    fetchUserList()
  }
}

const nextPage = () => {
  if (page.value < totalPages.value) {
    page.value++
    fetchUserList()
  }
}

// ====== ✅ 初始化:相当于 Vue 2 的 created ======
fetchUserList()

// ====== 搜索关键词变化时自动重新请求(防抖) ======
import { debounce } from 'lodash-es'
const debouncedSearch = debounce(() => {
  if (searchKeyword.value.trim()) {
    page.value = 1
    fetchUserList()
  }
}, 500)

watch(searchKeyword, () => {
  debouncedSearch()
})
</script>

4.2 场景二:详情页面(依赖路由参数)

Vue 3 + Vue Router 实现

<template>
  <div class="detail-page">
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <h1>{{ article.title }}</h1>
      <p>作者:{{ article.author }} | {{ article.createdAt }}</p>
      <div v-html="article.content"></div>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'

const route = useRoute()
const article = ref(null)
const loading = ref(false)
const error = ref(null)

// ====== 加载文章详情 ======
const fetchArticle = async (id) => {
  if (!id) return
  loading.value = true
  error.value = null
  try {
    const res = await axios.get(`/api/articles/${id}`)
    article.value = res.data
  } catch (err) {
    error.value = err.message || '加载文章失败'
  } finally {
    loading.value = false
  }
}

// ====== ✅ 初始化加载:在 setup 顶层调用 ======
fetchArticle(route.params.id)

// ====== 路由参数变化时重新加载 ======
watch(
  () => route.params.id,
  (newId, oldId) => {
    if (newId !== oldId) {
      fetchArticle(newId)
    }
  }
)
</script>

4.3 场景三:仪表盘(多个并行请求)

在实际项目中,一个页面往往需要同时发起多个接口请求

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

// ====== 各模块数据 ======
const stats = ref(null)
const chartData = ref(null)
const recentOrders = ref([])
const notifications = ref([])

// ====== 各模块加载状态 ======
const loadingStates = reactive({
  stats: false,
  chart: false,
  orders: false,
  notifications: false
})

// ====== 各模块错误状态 ======
const errors = reactive({
  stats: null,
  chart: null,
  orders: null,
  notifications: null
})

// ====== 并行请求所有数据 ======
const fetchDashboardData = async () => {
  // 并行发起所有请求
  const [statsRes, chartRes, ordersRes, notificationsRes] = await Promise.all([
    axios.get('/api/dashboard/stats').catch(err => ({ error: err })),
    axios.get('/api/dashboard/chart').catch(err => ({ error: err })),
    axios.get('/api/dashboard/orders').catch(err => ({ error: err })),
    axios.get('/api/dashboard/notifications').catch(err => ({ error: err }))
  ])

  // 处理统计
  loadingStates.stats = false
  if (statsRes.error) {
    errors.stats = statsRes.error.message
  } else {
    stats.value = statsRes.data
  }

  // 处理图表
  loadingStates.chart = false
  if (chartRes.error) {
    errors.chart = chartRes.error.message
  } else {
    chartData.value = chartRes.data
  }

  // 处理订单
  loadingStates.orders = false
  if (ordersRes.error) {
    errors.orders = ordersRes.error.message
  } else {
    recentOrders.value = ordersRes.data
  }

  // 处理通知
  loadingStates.notifications = false
  if (notificationsRes.error) {
    errors.notifications = notificationsRes.error.message
  } else {
    notifications.value = notificationsRes.data
  }
}

// ====== ✅ 初始化加载 ======
// 设置初始加载状态
loadingStates.stats = true
loadingStates.chart = true
loadingStates.orders = true
loadingStates.notifications = true

fetchDashboardData()
</script>

4.4 场景四:表单提交(非初始化请求)

注意:用户主动触发的操作(如表单提交、删除、更新)不应该放在生命周期中,而应该放在方法中由用户事件触发:

<script setup>
import { reactive, ref } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'

const form = reactive({
  name: '',
  email: '',
  content: ''
})

const submitting = ref(false)

// ====== ✅ 用户主动触发:放在方法中 ======
const handleSubmit = async () => {
  if (!form.name || !form.email) {
    ElMessage.warning('请填写完整信息')
    return
  }

  submitting.value = true
  try {
    await axios.post('/api/feedback', form)
    ElMessage.success('提交成功')
    // 重置表单
    Object.assign(form, { name: '', email: '', content: '' })
  } catch (err) {
    ElMessage.error(err.message || '提交失败')
  } finally {
    submitting.value = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.name" placeholder="姓名" />
    <input v-model="form.email" placeholder="邮箱" />
    <textarea v-model="form.content" placeholder="内容"></textarea>
    <button type="submit" :disabled="submitting">
      {{ submitting ? '提交中...' : '提交' }}
    </button>
  </form>
</template>

五、Vue 2 vs Vue 3 总结对比

对比维度 Vue 2(Options API) Vue 3(组合式 API)
数据请求位置 created 钩子中调用方法 setup() 顶层或 <script setup> 顶层直接调用
DOM 依赖请求 mounted 钩子中调用方法 onMounted() 中调用
请求方法定义 methods 选项中定义 直接定义为普通函数
加载状态管理 data 中定义 refreactive 定义
逻辑复用 Mixins(有缺陷) 组合式函数(Composables)
SSR 支持 created 支持,mounted 不支持 setup 支持,onMounted 不支持

六、决策流程图

需要发起接口请求?
│
├─ 是用户主动触发的操作(点击、提交等)?
│  └─ 是 ──→ 放在 methods / 普通函数中,由事件触发 ✅
│
├─ 需要在组件初始化时自动加载数据?
│  │
│  ├─ 需要操作 DOM 或依赖 DOM 渲染?
│  │  ├─ Vue 2:放在 mounted 中 ✅
│  │  └─ Vue 3:放在 onMounted 中 ✅
│  │
│  └─ 不需要操作 DOM(纯数据获取)?
│     ├─ Vue 2:放在 created 中 ✅
│     └─ Vue 3:在 setup 顶层直接调用 ✅
│
└─ 需要在数据变化时重新请求?
   └─ 使用 watch 监听变化,在回调中发起请求 ✅

七、注意事项与最佳实践

7.1 ⚠️ 不要在模板中直接调用请求方法

<!-- ❌ 错误:每次渲染都会发起请求 -->
<div>{{ fetchData() }}</div>

7.2 ⚠️ 处理组件卸载时的请求取消

在组件销毁时,应该取消未完成的请求,避免内存泄漏:

// Vue 3
import { ref, onMounted, onBeforeUnmount } from 'vue'

const controller = ref(null)

const fetchData = async () => {
  controller.value = new AbortController()
  try {
    const res = await axios.get('/api/data', {
      signal: controller.value.signal
    })
    // 处理数据
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('请求已取消')
    }
  }
}

onMounted(() => {
  fetchData()
})

onBeforeUnmount(() => {
  // 取消未完成的请求
  controller.value?.abort()
})

7.3 ✅ 使用 loading 和 error 状态提升用户体验

始终在请求中管理 loadingerror 状态,让用户知道当前请求的状态。

7.4 ✅ 封装统一的请求层

在实际项目中,建议封装统一的 request 模块,处理请求拦截、响应拦截、Token 管理、错误统一处理等。

八、总结

核心结论

  1. Vue 2 中:纯数据请求放在 created,依赖 DOM 的请求放在 mounted
  2. Vue 3 中:纯数据请求在 setup 顶层直接调用,依赖 DOM 的请求放在 onMounted
  3. 用户主动触发的操作(提交、删除等)放在方法中,不要放在生命周期里
  4. 逻辑复用:Vue 3 推荐使用组合式函数(Composables) 封装可复用的请求逻辑

理解这些规则,能让你在 Vue 项目中写出更加合理、高效的接口请求代码。

posted @ 2026-07-10 23:56  我有满天星辰  阅读(0)  评论(0)    收藏  举报