Vue 2 与 Vue 3 接口请求完全指南:数据请求应该写在哪里?
前言
在前几篇博客中,我们聊了 Vue 的响应式 API(ref/reactive)、侦听器(watch)和计算属性(computed)。今天,我们把目光投向每个 Vue 项目都绕不开的核心问题——接口请求应该写在哪里?
当你打开一个 Vue 项目,几乎每个页面组件都需要在初始化时从后端获取数据。那么问题来了:
- 请求应该写在
created还是mounted? - Vue 3 中
setup和onMounted怎么选? - 初始化数据的方法应该如何封装?
- 父子组件的数据请求顺序如何控制?
本文将系统梳理 Vue 2 和 Vue 3 中发起接口请求的最佳实践,通过大量代码案例和真实项目场景,帮你彻底搞清楚"请求应该写在哪"这个问题。
一、Vue 2 中的接口请求:created vs mounted
1.1 Vue 2 生命周期回顾
在 Vue 2 中,与数据请求最相关的生命周期钩子有两个:created 和 mounted。
| 生命周期 | 执行时机 | 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?
- 更早发起请求,减少白屏时间:
created在组件初始化后立即执行,远早于 DOM 的挂载。请求和 DOM 渲染可以并行进行,用户等待时间更短 - 逻辑解耦:数据获取是组件的内部逻辑,通常不依赖 DOM,放在
created中符合"关注点分离"原则 - 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()
}
}
场景三:父子组件数据依赖(重要!)
组件的加载顺序是:父组件先执行前三个生命周期(beforeCreate、created、beforeMount),然后执行子组件的生命周期,子组件全部挂载完毕后,父组件才执行 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.data 和 this.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 中定义 |
ref 或 reactive 定义 |
| 逻辑复用 | 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 状态提升用户体验
始终在请求中管理 loading 和 error 状态,让用户知道当前请求的状态。
7.4 ✅ 封装统一的请求层
在实际项目中,建议封装统一的 request 模块,处理请求拦截、响应拦截、Token 管理、错误统一处理等。
八、总结
核心结论:
- Vue 2 中:纯数据请求放在
created,依赖 DOM 的请求放在mounted - Vue 3 中:纯数据请求在
setup顶层直接调用,依赖 DOM 的请求放在onMounted中 - 用户主动触发的操作(提交、删除等)放在方法中,不要放在生命周期里
- 逻辑复用:Vue 3 推荐使用组合式函数(Composables) 封装可复用的请求逻辑
理解这些规则,能让你在 Vue 项目中写出更加合理、高效的接口请求代码。

浙公网安备 33010602011771号