PreFetch 是一項功能(僅在使用 Quasar CLI 時可用),允許 Vue Router 拾取的元件(在 /src/router/routes.js
中定義)
- 預先抓取資料
- 驗證路由
- 在不符合某些條件(例如使用者未登入)時,重新導向到另一個路由
- 可以協助初始化 Store 狀態
以上所有操作都會在實際路由元件呈現之前執行。
它旨在與所有 Quasar 模式(SPA、PWA、SSR、Cordova、Electron)一起使用,但對於 SSR 建置特別有用。
安裝
return {
preFetch: true
}
警告
當您使用它來預先抓取資料時,您需要使用 Vuex Store,因此請確保您的專案資料夾在建立專案時具有 /src/store
資料夾,否則請產生一個新專案並將 store 資料夾內容複製到您目前的專案。
PreFetch 如何協助 SSR 模式
此功能對於 SSR 模式特別有用(但不限於此)。在 SSR 期間,我們基本上是呈現應用程式的「快照」,因此如果應用程式依賴某些非同步資料,則需要在我們開始呈現程序之前預先抓取並解析此資料。
另一個考量是,在用戶端,在我們掛載用戶端應用程式之前,需要提供相同的資料 - 否則用戶端應用程式將使用不同的狀態呈現,並且 hydration 將會失敗。
為了處理這個問題,抓取的資料需要存在於視圖元件之外,在專用的資料儲存區或「狀態容器」中。在伺服器上,我們可以在呈現之前預先抓取資料並將資料填入儲存區。用戶端儲存區將在我們掛載應用程式之前直接擷取伺服器狀態。
PreFetch 何時啟動
preFetch
hook(在接下來的章節中描述)由造訪的路由決定 - 這也決定了呈現哪些元件。事實上,給定路由所需的資料也是該路由呈現的元件所需的資料。因此,將 hook 邏輯僅放在路由元件內是自然(也是必要的)。 這包括 /src/App.vue
,在這種情況下,它只會在應用程式啟動時執行一次。
讓我們舉一個例子,以便了解何時呼叫 hook。假設我們有這些路由,並且我們為所有這些元件編寫了 preFetch
hook
[
{
path: '/',
component: LandingPage
},
{
path: '/shop',
component: ShopLayout,
children: [
{
path: 'all',
component: ShopAll
},
{
path: 'new',
component: ShopNew
},
{
path: 'product/:name',
component: ShopProduct,
children: [{
path: 'overview',
component: ShopProductOverview
}]
}
]
}
]
現在,讓我們看看當使用者按照下面指定的順序依序造訪這些路由時,如何呼叫 hook。
正在造訪的路由 | 從以下位置呼叫的 Hooks | 觀察 |
---|---|---|
/ | App.vue 然後 LandingPage | App.vue hook 在我們的應用程式啟動時呼叫。 |
/shop/all | ShopLayout 然後 ShopAll | - |
/shop/new | ShopNew | ShopNew 是 ShopLayout 的子元件,而 ShopLayout 已經呈現,因此 ShopLayout 不會再次呼叫。 |
/shop/product/pyjamas | ShopProduct | - |
/shop/product/shoes | ShopProduct | Quasar 注意到相同的元件已呈現,但路由已更新,並且它具有路由參數,因此它再次呼叫 hook。 |
/shop/product/shoes/overview | ShopProduct 然後 ShopProductOverview | ShopProduct 具有路由參數,因此即使它已經呈現,也會呼叫它。 |
/ | LandingPage | - |
用法
hook 定義為路由元件上稱為 preFetch
的自訂靜態函式。請注意,由於此函式將在元件實例化之前呼叫,因此它無法存取 this
。
<template>
<div>{{ item.title }}</div>
</template>
<script>
import { useStore } from 'vuex'
export default {
// our hook here
preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext, urlPath, publicPath }) {
// fetch data, validate route and optionally redirect to some other route...
// ssrContext is available only server-side in SSR mode
// No access to "this" here
// Return a Promise if you are running an async job
// Example:
return store.dispatch('fetchItem', currentRoute.params.id)
},
setup () {
const $store = useStore()
// display the item from store state.
const item = computed(() => $store.state.items[this.$route.params.id])
return { item }
}
}
</script>
如果您使用 <script setup>
(和 Vue 3.3+)
<script setup>
/**
* The defineOptions is a macro.
* The options will be hoisted to module scope and cannot access local
* variables in <script setup> that are not literal constants.
*/
defineOptions({
preFetch () {
console.log('running preFetch')
}
})
</script>
提示
如果您正在開發 SSR 應用程式,則可以查看伺服器端提供的 ssrContext 物件。
// related action for Promise example
// ...
actions: {
fetchItem ({ commit }, id) {
return axiosInstance.get(url, id).then(({ data }) => {
commit('mutation', data)
})
}
}
// ...
重新導向範例
以下是在某些情況下重新導向使用者的範例,例如當他們嘗試存取只有經過身份驗證的使用者才能看到的頁面時。
// We assume here we already wrote the authentication logic
// in the Vuex Store, so take as a high-level example only.
preFetch ({ store, redirect }) {
if (!store.state.authenticated) {
redirect({ path: '/login' })
}
}
預設情況下,重新導向會以 302 的狀態回應代碼發生,但我們可以在呼叫函式時將此狀態代碼作為第二個選用參數傳遞,如下所示
redirect({ path: '/moved-permanently' }, 301)
如果呼叫 redirect(false)
(僅在用戶端支援!),它會中止目前的路由導航。請注意,如果您在 src/App.vue
中這樣使用它,它將會暫停應用程式啟動,這是不可取的。
redirect()
方法需要 Vue Router 位置物件。
使用 preFetch 初始化 Pinia 或 Vuex Store
preFetch
hook 只會在應用程式啟動時執行一次,因此您可以利用這個機會在這裡初始化 Pinia store 或 Vuex Store。
// App.vue - handling Pinia stores
// example with a store named "myStore"
// placed in /src/stores/myStore.js|ts
import { useMyStore } from 'stores/myStore'
export default {
// ...
preFetch () {
const myStore = useMyStore()
// do something with myStore
}
}
// App.vue
export default {
// ...
preFetch ({ store }) {
// initialize something in store here
}
}
Vuex Store 代碼拆分
在大型應用程式中,您的 Vuex store 很可能會拆分為多個模組。當然,也可以將這些模組代碼拆分為對應的路由元件區塊。假設我們有以下 store 模組
// we've merged everything into one file here;
// an initialized Quasar project splits every component of a Vuex module
// into separate files, but for the sake of the example
// here in the docs, we show this module as a single file
export default {
namespaced: true,
// IMPORTANT: state must be a function so the module can be
// instantiated multiple times
state: () => ({
count: 0
}),
actions: {
inc: ({ commit }) => commit('inc')
},
mutations: {
inc: state => state.count++
}
}
現在,我們可以使用 store.registerModule()
在路由元件的 preFetch()
hook 中延遲註冊此模組
<template>
<div>{{ fooCount }}</div>
</template>
<script>
import { useStore } from 'vuex'
import { onMounted, onUnmounted } from 'vue'
// import the module here instead of in `src/store/index.js`
import fooStoreModule from 'store/foo'
export default {
preFetch ({ store }) {
store.registerModule('foo', fooStoreModule)
return store.dispatch('foo/inc')
},
setup () {
const $store = useStore()
onMounted(() => {
// Preserve the previous state if it was injected from the server
$store.registerModule('foo', fooStoreModule, { preserveState: true })
})
onUnmounted(() => {
// IMPORTANT: avoid duplicate module registration on the client
// when the route is visited multiple times.
$store.unregisterModule('foo')
})
const fooCount = computed(() => {
return $store.state.foo.count
})
return {
fooCount
}
}
}
</script>
另請注意,由於該模組現在是路由元件的相依性,因此 Webpack 會將其移至路由元件的非同步區塊中。
警告
別忘了對 registerModule
使用 preserveState: true
選項,以便我們保留伺服器注入的狀態。
搭配 Vuex 和 TypeScript 使用
您可以使用 preFetch
輔助程式來輸入提示 store 參數(否則 store 參數將具有 any
類型)
import { preFetch } from 'quasar/wrappers'
import { Store } from 'vuex'
interface StateInterface {
// ...
}
export default {
preFetch: preFetch<StateInterface>(({ store }) => {
// Do something with your newly-typed store parameter
}),
}
提示
這僅適用於輸入 store
參數,即使使用一般語法,其他參數也會自動輸入。
載入狀態
良好的 UX 包括在使用者等待頁面準備就緒時,通知使用者正在背景中進行某些工作。Quasar CLI 提供兩種開箱即用的選項。
LoadingBar
當您將 Quasar LoadingBar 外掛程式新增至您的應用程式時,Quasar CLI 將在預設情況下在執行 preFetch hook 時使用它。
Loading
也可以使用 Quasar Loading 外掛程式。以下是一個範例
import { Loading } from 'quasar'
export default {
// ...
preFetch ({ /* ... */ }) {
Loading.show()
return new Promise(resolve => {
// do something async here
// then call "resolve()"
}).then(() => {
Loading.hide()
})
}
}