PreFetch 是一个特性(仅在使用 Quasar CLI 时可用),它允许被 Vue Router 选中的组件(定义在 /src/router/routes.js 中)能够:
- pre-fetch data
- validate the route
- redirect to another route, when some conditions aren’t met (like user isn’t logged in)
- can help in initializing the Store state
以上所有操作都会在实际路由组件渲染之前运行。
It is designed to work with all Quasar modes (SPA, PWA, SSR, Cordova, Electron), but it is especially useful for SSR builds.
Installation
return {
preFetch: true
}WARNING
When you use it to pre-fetch data, you may want to use Pinia, so make sure that your project folder has the /src/stores (for Pinia) folders when you create your project, otherwise generate a new project and copy the store folder contents to your current project (or use quasar new store command).
How PreFetch Helps SSR Mode
这个特性对 SSR 模式特别有用(但不仅限于此)。在 SSR 中,我们本质上是在渲染应用的一个"快照",所以如果应用依赖于某些异步数据,那么这些数据需要在开始渲染过程之前被预取和解析。
另一个问题是,在客户端,相同的数据需要在挂载客户端应用之前就可用——否则客户端应用会使用不同的状态进行渲染,导致 hydration 失败。
为了解决这个问题,获取的数据需要存放在视图组件之外的专用数据存储或"状态容器"中。在服务端,我们可以在渲染之前预取数据并填充到 store 中。客户端的 store 会在挂载应用之前直接获取服务端的状态。
When PreFetch Gets Activated
The preFetch hook (described in next sections) is determined by the route visited - which also determines what components are rendered. In fact, the data needed for a given route is also the data needed by the components rendered at that route. So it is natural (and also required) to place the hook logic ONLY inside route components. This includes /src/App.vue, which in this case will run only once at the app bootup.
Let’s take an example in order to understand when the hook is being called. Let’s say we have these routes and we’ve written preFetch hooks for all these components:
;[
{
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
}
]
}
]
}
]现在来看看当用户按照下面指定的顺序依次访问这些路由时,钩子是如何被调用的。
| Route being visited | Hooks called from | Observations |
|---|---|---|
/ | App.vue then LandingPage | App.vue hook is called since our app boots up. |
/shop/all | ShopLayout then ShopAll | - |
/shop/new | ShopNew | ShopNew is a child of ShopLayout, and ShopLayout is already rendered, so ShopLayout isn’t called again. |
/shop/product/pyjamas | ShopProduct | - |
/shop/product/shoes | ShopProduct | Quasar notices the same component is already rendered, but the route has been updated and it has route params, so it calls the hook again. |
/shop/product/shoes/overview | ShopProduct then ShopProductOverview | ShopProduct has route params so it is called even though it’s already rendered. |
/ | LandingPage | - |
Usage
The hook is defined as a custom static function called preFetch on our route components. Note that because this function will be called before the components are instantiated, it doesn’t have access to this.
以下是使用 Pinia 时的示例:
<template>
<div>{{ item.title }}</div>
</template>
<script>
import { useRoute } from 'vue-router'
import { useMyStore } from 'stores/myStore.js'
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:
const myStore = useMyStore() // useMyStore(store) for SSR
return myStore.fetchItem(currentRoute.params.id) // assumes it is async
},
setup() {
const myStore = useMyStore()
const $route = useRoute()
// display the item from store state.
const item = computed(() => myStore.items[$route.params.id])
return { item }
}
}
</script>If you are using <script setup> (and 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>TIP
如果你正在开发 SSR 应用,可以查看 ssrContext Object that gets supplied server-side.
// related action for Promise example
// ...
actions: {
fetchItem ({ commit }, id) {
return axiosInstance.get(url, id).then(({ data }) => {
this.items = data
})
}
}
// ...Redirecting Example
WARNING
请注意重定向时不要让应用进入无限重定向循环。
WARNING
Please remember to return from the function immediately after calling redirect().
以下是在某些情况下重定向用户的示例,比如当他们尝试访问仅认证用户才能查看的页面时。
// We assume here we already wrote the authentication logic
// in one Pinia Store, so take as a high-level example only.
import { useMyStore } from 'stores/myStore'
preFetch ({ store, redirect }) {
const myStore = useMyStore() // useMyStore(store) for SSR
if (!myStore.isAuthenticated) {
redirect({ path: '/login' })
return
}
}以下是它的定义:
readonly redirect: (
url: string | RouteLocationRaw,
/**
* HTTP status code to use for the redirection.
* Only used in SSR mode.
*
* @default 302
*/
httpStatusCode?: HttpRedirectStatusCode
) => void;The redirect() method accepts a String (full URL) or a Vue Router location String or Object. On SSR it can receive a second parameter which should be a Number for any of the HTTP STATUS codes that redirect the browser (3xx ones).
// Examples for redirect() with a Vue Router location:
redirect('/1') // Vue Router location as String
redirect({ path: '/1' }) // Vue Router location as Object
// Example for redirect() with a URL:
redirect('https://quasar.dev')IMPORTANT!
Vue Router 的 location(字符串或对象形式)不是指 URL 路径(和 hash),而是你定义的实际 Vue Router 路由。 所以不要添加 publicPath,如果你使用 Vue Router hash 模式也不要添加 hash。
Let’s say that we have this Vue Router route defined:
{
path: '/one',
component: PageOne
}
Then regardless of our publicPath we can call redirect() like this:
// publicPath: /wiki; vueRouterMode: history
redirect('/one') // good way
redirect({ path: '/one' }) // good way
redirect('/wiki/one') // WRONG!
// publicPath: /wiki; vueRouterMode: hash
redirect('/one') // good way
redirect({ path: '/one' }) // good way
redirect('/wiki/#/one') // WRONG!
// no publicPath; vueRouterMode: hash
redirect('/one') // good way
redirect({ path: '/one' }) // good way
redirect('/#/one') // WRONG!Using preFetch to Initialize Pinia
The preFetch hook runs only once, when the app boots up, so you can use this opportunity to initialize the Pinia store(s) here.
// App.vue - handling Pinia stores
// example with a store named "myStore"
// placed in /src/stores/myStore.js|ts
import { useMyStore } from 'stores/myStore.js'
export default {
// ...
preFetch () {
const myStore = useMyStore()
// do something with myStore
}
}Loading State
良好的用户体验包括在后台处理时通知用户正在进行操作。Quasar CLI 为此提供了两个开箱即用的选项。
LoadingBar
当你将 Quasar LoadingBar 插件添加到应用时,Quasar CLI 默认会在运行 preFetch 钩子时使用它。
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()
})
}
}