预取是一个功能(**仅在使用 Quasar CLI 时可用**),它允许 Vue Router 捕获的组件(在 /src/router/routes.js
中定义)来
- 预取数据
- 验证路由
- 在某些条件不满足时(例如用户未登录)重定向到另一个路由
- 可以帮助初始化 Store 状态
所有这些操作将在实际渲染路由组件之前执行。
**它设计用于所有 Quasar 模式**(SPA、PWA、SSR、Cordova、Electron),但它对 SSR 构建尤其有用。
安装
return {
preFetch: true
}
警告
当您使用它来预取数据时,需要使用 Vuex Store,因此请确保您的项目文件夹在创建项目时具有 /src/store
文件夹,否则请生成一个新项目并将 store 文件夹内容复制到当前项目。
预取如何帮助 SSR 模式
此功能对于 SSR 模式尤其有用(但不仅限于此)。在 SSR 期间,我们实际上是在渲染应用程序的“快照”,因此如果应用程序依赖于某些异步数据,**则需要在开始渲染过程之前预取并解析此数据**。
另一个问题是在客户端,相同的数据需要在挂载客户端应用程序之前可用 - 否则客户端应用程序将使用不同的状态进行渲染,并且水合将失败。
为了解决这个问题,获取的数据需要存在于视图组件之外,在专用数据存储或“状态容器”中。在服务器端,我们可以在渲染之前预取并将数据填充到存储中。客户端存储将在我们挂载应用程序之前直接获取服务器状态。
何时激活预取
preFetch
钩子(在下一节中描述)由访问的路由决定 - 这也决定了渲染哪些组件。事实上,给定路由所需的数据也是在该路由处渲染的组件所需的数据。**因此,将钩子逻辑仅放置在路由组件内部是自然而然的(也是必需的)。** 这包括 /src/App.vue
,在这种情况下,它只会在应用程序启动时运行一次。
让我们举一个例子来理解何时调用钩子。假设我们有以下路由,并且我们为所有这些组件编写了 preFetch
钩子
[
{
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
}]
}
]
}
]
现在,让我们看看当用户按以下顺序依次访问这些路由时,钩子是如何被调用的。
正在访问的路由 | 来自的钩子调用 | 观察结果 |
---|---|---|
/ | App.vue 然后 LandingPage | 由于我们的应用程序启动,因此调用了 App.vue 钩子。 |
/shop/all | ShopLayout 然后 ShopAll | - |
/shop/new | ShopNew | ShopNew 是 ShopLayout 的子组件,并且 ShopLayout 已经渲染,因此不会再次调用 ShopLayout。 |
/shop/product/pyjamas | ShopProduct | - |
/shop/product/shoes | ShopProduct | Quasar 注意到相同的组件已渲染,但路由已更新并且具有路由参数,因此它会再次调用钩子。 |
/shop/product/shoes/overview | ShopProduct 然后 ShopProductOverview | ShopProduct 具有路由参数,因此即使它已渲染也会被调用。 |
/ | LandingPage | - |
用法
钩子定义为我们路由组件上的名为 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(s)
preFetch
钩子仅运行一次,在应用程序启动时,因此您可以利用此机会在此处初始化 Pinia store(s) 或 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()
钩子中延迟注册此模块
<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
参数添加类型,即使使用普通语法,其他参数也会自动添加类型。
加载状态
良好的用户体验包括在用户等待页面加载时,通知用户后台正在处理某些操作。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()
})
}
}