跳至主要內容
Skip to content

接口請求一般放在哪個生命週期?為什麼?

這題表面上是在問生命週期,實際上是在考你能不能根據渲染模式、資料依賴、路由參數與使用者體驗來決定請求時機。

一句話回答:在純 CSR 的 Vue 頁面中,頁面初始化接口請求通常放在 onMounted,因為這時組件已經掛載,適合觸發瀏覽器端副作用;但如果請求不依賴 DOM,也可以封裝在 setup 裡的 composable 或 store action。若是 SSR 場景,應使用 onServerPrefetch 或框架提供的資料預取機制。


一、 必考觀念

1. 為什麼常放在 onMounted

在 CSR 中,接口請求屬於副作用。onMounted 表示組件已經掛載到瀏覽器 DOM 上,這時執行瀏覽器端副作用比較安全。

vue
<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,由不同時機呼叫。

例如:

typescript
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 常見寫法:

javascript
export default {
  data() {
    return {
      user: null,
      loading: false,
    }
  },
  async mounted() {
    this.loading = true
    this.user = await fetchUser()
    this.loading = false
  },
}

Composition API 對應:

typescript
onMounted(async () => {
  user.value = await fetchUser()
})

如果不需要 DOM,Vue 2 有時也會放在 created。但在 Vue 3 Composition API 裡,更常見的是把請求邏輯放到 composable/store,再由 onMounted、watch 或路由守衛觸發。


二、 不同場景怎麼放?

1. 頁面首次載入資料

CSR 頁面最常見:

typescript
onMounted(() => {
  loadData()
})

適合:

  • 後台頁面。
  • 登入後工具。
  • SEO 不重要。
  • 不需要 SSR 首屏 HTML 直接帶資料。

2. 依賴路由參數

如果從 /user/1 切到 /user/2,同一個頁面組件可能被復用,onMounted 不會再執行。

這時要 watch route params:

typescript
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 不會包含資料。

這時應該使用:

typescript
onServerPrefetch(async () => {
  product.value = await fetchProduct()
})

或使用 Nuxt / Vite SSR / 路由框架提供的資料預取機制。

延伸讀:Vue SSR 的實現原理

4. 使用者操作後才請求

有些請求不該跟生命週期綁死,而是由事件觸發:

vue
<button @click="submit">送出</button>
typescript
async function submit() {
  await createOrder(form.value)
}

例如:

  • 表單提交。
  • 點擊搜尋。
  • 打開 Modal 後載入內容。
  • 滾動到底載入更多。

這些不需要放在 mounted。

5. 全域狀態或跨頁快取

如果資料多個頁面都需要,可以放在 Pinia action:

typescript
export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null,
  }),
  actions: {
    async fetchProfile() {
      if (this.profile) {
        return
      }

      this.profile = await fetchProfile()
    },
  },
})

頁面中:

typescript
const userStore = useUserStore()

onMounted(() => {
  userStore.fetchProfile()
})

這樣請求邏輯集中在資料層,頁面只決定觸發時機。


三、 為什麼不建議無腦放在 setup 頂層?

你可能會看到:

typescript
const user = await fetchUser()

這在 <script setup> 的 top-level await 或配合 Suspense 時可以成立,但不是所有場景都適合。

需要考慮:

  • 是否會阻塞組件 setup?
  • 是否需要 Suspense 邊界?
  • 是否在 SSR 和 CSR 都要執行?
  • 錯誤和 loading 如何呈現?
  • 請求是否可能重複?

比較穩的做法是把請求封裝成可控函數:

typescript
const {
  data,
  loading,
  error,
  execute,
} = useRequest(fetchUser)

onMounted(execute)

這樣呼叫時機比較清楚。


四、 請求要處理哪些狀態?

1. Loading

typescript
const loading = ref(false)

async function loadData() {
  loading.value = true
  try {
    data.value = await fetchData()
  } finally {
    loading.value = false
  }
}

2. Error

typescript
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

如果請求可能快速連續觸發,要避免舊請求覆蓋新請求。

typescript
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

typescript
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。

typescript
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 中,createdmounted 更早,資料、methods 已可用,但 DOM 還沒有掛載。

如果請求不依賴 DOM,放 created 可以更早開始請求。

但在 SSR 中,created 可能在 server 和 client 都涉及執行時機問題;在 Vue 3 中更常用 setuponMountedonServerPrefetch 或框架資料預取機制來表達意圖。

Q3:onMounted 中請求會不會影響首屏?

會。

CSR 中如果首屏內容依賴 onMounted 請求,使用者可能先看到 loading 或空狀態。這是 SPA 常見模式。

如果希望 HTML 回來時就有內容,可以考慮 SSR、SSG 或預渲染。

延伸讀:Vue SPA 如何優化首屏載入速度

Q4:切換路由參數後為什麼 mounted 不重新執行?

因為 Vue Router 可能復用同一個組件實例。

例如:

text
/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。
vue
<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 不依賴 DOMcreatedmounted,視需求
依賴 route paramswatch(() => 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。核心是根據資料依賴、渲染模式和使用者體驗決定請求時機。

延伸閱讀