接口請求一般放在哪個生命週期?為什麼?
這題表面上是在問生命週期,實際上是在考你能不能根據渲染模式、資料依賴、路由參數與使用者體驗來決定請求時機。
一句話回答:在純 CSR 的 Vue 頁面中,頁面初始化接口請求通常放在 onMounted,因為這時組件已經掛載,適合觸發瀏覽器端副作用;但如果請求不依賴 DOM,也可以封裝在 setup 裡的 composable 或 store action。若是 SSR 場景,應使用 onServerPrefetch 或框架提供的資料預取機制。
一、 必考觀念
1. 為什麼常放在 onMounted?
在 CSR 中,接口請求屬於副作用。onMounted 表示組件已經掛載到瀏覽器 DOM 上,這時執行瀏覽器端副作用比較安全。
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const user = ref(null)
const loading = ref(false)
const error = ref<Error | null>(null)
onMounted(async () => {
loading.value = true
try {
user.value = await fetchUser()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
})
</script>這樣做的理由:
onMounted只在瀏覽器端執行,避免 SSR 中誤用瀏覽器 API。- 組件已經建立完成,可以安全更新本地狀態。
- 若請求後需要操作 DOM,也能配合
nextTick。 - 語意清楚:組件掛載後載入資料。
2. 但不是所有請求都必須放 onMounted
如果請求不依賴 DOM,其實可以抽成函數、composable 或 store action,由不同時機呼叫。
例如:
async function loadUser() {
loading.value = true
try {
user.value = await fetchUser()
} finally {
loading.value = false
}
}
onMounted(loadUser)這比把所有邏輯塞進 onMounted 更好測試,也更容易重用。
3. Options API 對應哪個生命週期?
Vue 2 / Options API 常見寫法:
export default {
data() {
return {
user: null,
loading: false,
}
},
async mounted() {
this.loading = true
this.user = await fetchUser()
this.loading = false
},
}Composition API 對應:
onMounted(async () => {
user.value = await fetchUser()
})如果不需要 DOM,Vue 2 有時也會放在 created。但在 Vue 3 Composition API 裡,更常見的是把請求邏輯放到 composable/store,再由 onMounted、watch 或路由守衛觸發。
二、 不同場景怎麼放?
1. 頁面首次載入資料
CSR 頁面最常見:
onMounted(() => {
loadData()
})適合:
- 後台頁面。
- 登入後工具。
- SEO 不重要。
- 不需要 SSR 首屏 HTML 直接帶資料。
2. 依賴路由參數
如果從 /user/1 切到 /user/2,同一個頁面組件可能被復用,onMounted 不會再執行。
這時要 watch route params:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params.id,
async (id) => {
user.value = await fetchUser(String(id))
},
{ immediate: true },
)immediate: true 的作用是:
- 初始化時請求一次。
- params 改變時再請求。
延伸讀:watch 與 computed 的區別。
3. 需要 SSR 首屏資料
如果是 SSR 頁面,onMounted 不會在 server 執行,因此 server render 出來的 HTML 不會包含資料。
這時應該使用:
onServerPrefetch(async () => {
product.value = await fetchProduct()
})或使用 Nuxt / Vite SSR / 路由框架提供的資料預取機制。
延伸讀:Vue SSR 的實現原理。
4. 使用者操作後才請求
有些請求不該跟生命週期綁死,而是由事件觸發:
<button @click="submit">送出</button>async function submit() {
await createOrder(form.value)
}例如:
- 表單提交。
- 點擊搜尋。
- 打開 Modal 後載入內容。
- 滾動到底載入更多。
這些不需要放在 mounted。
5. 全域狀態或跨頁快取
如果資料多個頁面都需要,可以放在 Pinia action:
export const useUserStore = defineStore('user', {
state: () => ({
profile: null,
}),
actions: {
async fetchProfile() {
if (this.profile) {
return
}
this.profile = await fetchProfile()
},
},
})頁面中:
const userStore = useUserStore()
onMounted(() => {
userStore.fetchProfile()
})這樣請求邏輯集中在資料層,頁面只決定觸發時機。
三、 為什麼不建議無腦放在 setup 頂層?
你可能會看到:
const user = await fetchUser()這在 <script setup> 的 top-level await 或配合 Suspense 時可以成立,但不是所有場景都適合。
需要考慮:
- 是否會阻塞組件 setup?
- 是否需要 Suspense 邊界?
- 是否在 SSR 和 CSR 都要執行?
- 錯誤和 loading 如何呈現?
- 請求是否可能重複?
比較穩的做法是把請求封裝成可控函數:
const {
data,
loading,
error,
execute,
} = useRequest(fetchUser)
onMounted(execute)這樣呼叫時機比較清楚。
四、 請求要處理哪些狀態?
1. Loading
const loading = ref(false)
async function loadData() {
loading.value = true
try {
data.value = await fetchData()
} finally {
loading.value = false
}
}2. Error
const error = ref<Error | null>(null)
async function loadData() {
error.value = null
try {
data.value = await fetchData()
} catch (err) {
error.value = err as Error
}
}3. Race Condition
如果請求可能快速連續觸發,要避免舊請求覆蓋新請求。
let requestId = 0
async function loadData(keyword: string) {
const id = ++requestId
const result = await search(keyword)
if (id !== requestId) {
return
}
results.value = result
}4. 取消請求
使用 AbortController:
watch(keyword, async (value, _oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
results.value = await fetch(`/api/search?q=${value}`, {
signal: controller.signal,
}).then(res => res.json())
})5. 組件卸載後避免更新
如果請求回來時組件已經卸載,應避免繼續更新狀態或操作 DOM。
let disposed = false
onUnmounted(() => {
disposed = true
})
async function loadData() {
const result = await fetchData()
if (disposed) {
return
}
data.value = result
}更好的方式通常是用 AbortController 取消請求。
五、 追問題庫
Q1:接口請求為什麼不放在 beforeMount?
可以,但通常沒有必要。
beforeMount 時組件還沒掛載到 DOM。大多數 CSR 請求放 onMounted 語意更清楚。如果請求完全不依賴 DOM,與其糾結 beforeMount,更推薦抽成 composable/store,由明確時機觸發。
Q2:為什麼 Vue 2 有些人放 created?
Vue 2 Options API 中,created 比 mounted 更早,資料、methods 已可用,但 DOM 還沒有掛載。
如果請求不依賴 DOM,放 created 可以更早開始請求。
但在 SSR 中,created 可能在 server 和 client 都涉及執行時機問題;在 Vue 3 中更常用 setup、onMounted、onServerPrefetch 或框架資料預取機制來表達意圖。
Q3:onMounted 中請求會不會影響首屏?
會。
CSR 中如果首屏內容依賴 onMounted 請求,使用者可能先看到 loading 或空狀態。這是 SPA 常見模式。
如果希望 HTML 回來時就有內容,可以考慮 SSR、SSG 或預渲染。
延伸讀:Vue SPA 如何優化首屏載入速度。
Q4:切換路由參數後為什麼 mounted 不重新執行?
因為 Vue Router 可能復用同一個組件實例。
例如:
/user/1 -> /user/2如果 route record 對應同一個 UserPage,Vue 可能只更新 route params,不重新 mount。這時要 watch params 或使用路由守衛。
Q5:接口請求應該寫在組件還是 store?
看資料歸屬。
如果是頁面私有資料,可以在頁面組件或 composable 中處理。
如果是多處共享資料,例如使用者資訊、權限、全站設定,應該放 store action 或資料服務層。
六、 實作題
題目:UserDetail 頁面應該怎麼請求資料?
需求:
- 路由是
/user/:id。 - 首次進頁面要載入 user。
- 從
/user/1切到/user/2時要重新載入。 - 要處理 loading/error。
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const user = ref(null)
const loading = ref(false)
const error = ref<Error | null>(null)
watch(
() => route.params.id,
async (id) => {
loading.value = true
error.value = null
try {
user.value = await fetchUser(String(id))
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
},
{ immediate: true },
)
</script>為什麼不用只放 onMounted?
因為同一個路由組件可能被復用,params 改變時不一定重新 mount。
七、 資深視角
1. 生命週期不是資料層架構
把請求放在哪個生命週期,只是觸發時機問題。
更重要的是:
- 請求邏輯是否可重用?
- loading/error 是否標準化?
- 是否有快取?
- 是否會重複請求?
- 是否能取消?
- 是否支援 SSR?
成熟專案通常會把請求封裝在:
- API service。
- composable。
- Pinia action。
- query/cache library。
- SSR data loader。
2. CSR 和 SSR 答案不同
如果面試官問的是普通 Vue SPA,回答 onMounted 很合理。
如果問的是 SSR / Nuxt,回答就要變成:
- server 階段使用
onServerPrefetch或框架資料載入。 - client hydration 時恢復 initial state。
- 避免 server/client 重複請求與 hydration mismatch。
3. 請求時機要服務使用者體驗
有些資料要阻塞首屏,有些可以延後。
例如:
- 商品詳情核心資料:優先載入。
- 推薦商品:可以延後。
- 評論:可以分頁或滾動載入。
- 圖表:可以組件懶載入。
這比「所有接口都 mounted 裡請求」更接近真實工程。
總結
| 場景 | 建議位置 |
|---|---|
| 普通 CSR 頁面首次資料 | onMounted |
| Vue 2 Options API 不依賴 DOM | created 或 mounted,視需求 |
| 依賴 route params | watch(() => route.params.id, ..., { immediate: true }) |
| SSR 首屏資料 | onServerPrefetch 或框架資料預取 |
| 使用者操作 | 事件 handler |
| 多處共享資料 | store action / composable / service |
| 需要 DOM 後再請求或操作 | onMounted + 必要時 nextTick |
一句完整的面試回答可以是:
在普通 CSR 的 Vue 頁面中,接口請求一般放在
onMounted,因為它是瀏覽器端副作用,組件掛載後再請求語意清楚,也能安全處理 DOM 相關邏輯。但這不是絕對規則:如果請求依賴路由參數,應該 watch route params 並設immediate;如果是 SSR,需要用onServerPrefetch或框架的資料預取機制;如果是使用者操作才需要的資料,就放在事件 handler;如果是多處共享資料,應該封裝到 store action 或 composable。核心是根據資料依賴、渲染模式和使用者體驗決定請求時機。