预取(PreFetch)是一个(仅在使用 Quasar CLI 时可用)特性,它可以让被 Vue Router 选中的组件(在 /src/router/routes.js 中定义的组件)拥有以下额外能力:
- 预取数据
- 验证路由
- 当不满足条件时进行路由重定向(例如用户未登录时)
- 帮助初始化 Store 数据
上述所有工作都会在实际的路由组件渲染之前完成。
这个特性在所有的 Quasar 开发模式中都可用(SPA、PWA、SSR、Cordova、Electron),但它对 SSR 构建尤其有用。
安装
return {
preFetch: true,
};WARNING
当您使用预取来获取数据时,您需要使用 Pinia 来存储数据,所以请确保在创建项目时项目中已经包含了 /src/stores 目录,否则请创建一个新的项目并将 store 目录的内容复制到您当前的项目中。
为什么 PreFetch 对 SSR 很有用
这个特性对 SSR 开发模式尤其有用(但并不仅限于 SSR 模式)。在 SSR 中,我们实质上是在渲染应用的一个"快照",所以当应用依赖一些异步数据时,这些数据需要在渲染过程开始之前预取并准备好。
另一个问题是,在客户端,同样的数据需要在挂载客户端应用之前就可用——否则客户端应用会使用不同的状态进行渲染,导致水合(hydration)失败。
为了解决这个问题,获取的数据需要存放在视图组件之外,放在一个专门的数据仓库(data store)或"状态容器"中。在服务端,我们可以在渲染之前预取数据并填充到 store 中。客户端的 store 则会在挂载应用之前直接接收服务端的状态。
PreFetch 钩子何时被激活
preFetch 钩子(将在下一节中详细介绍)由访问的路由决定——而路由同时也决定了哪些组件会被渲染。事实上,给定路由所需的数据,也正是在该路由上渲染的组件所需的数据。因此,将 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 { useRoute } from "vue-router";
import { useMyStore } from "stores/myStore";
export default {
// 我们的钩子在这里
preFetch({ store, currentRoute, previousRoute, redirect, ssrContext, urlPath, publicPath }) {
// 在这里可以获取数据、验证路由、重定向路由等等...
// ssrContext 仅在 SSR 模式的服务端可用
// 这里无法访问 "this"
// 如果执行了异步任务,请返回一个 Promise
// 示例:
const myStore = useMyStore(); // SSR 模式下使用 useMyStore(store)
return myStore.fetchItem(currentRoute.params.id); // 假设这是一个异步操作
},
setup() {
const myStore = useMyStore();
const $route = useRoute();
// 展示 store 中的 item 数据
const item = computed(() => myStore.items[$route.params.id]);
return { item };
},
};
</script>如果您在使用 <script setup>(且 Vue 3.3+):
<script setup>
/**
* defineOptions 是一个宏。
* 选项将会被提升到模块作用域中,
* 无法访问 <script setup> 中不是字面常量的局部变量。
*/
defineOptions({
preFetch({ store }) {
console.log("running preFetch");
},
});
</script>TIP
如果您在开发 SSR 应用,可以查看服务端提供的 ssrContext 对象。
// 异步任务相关的 action 示例
// ...
actions: {
fetchItem (id) {
return axiosInstance.get(url, id).then(({ data }) => {
this.items = data
})
}
}
// ...重定向示例
下面是在某些情况下重定向用户的示例,比如当用户试图访问仅限已认证用户查看的页面时。
// 这里假设我们已经在一个 Pinia Store 中编写了身份验证逻辑,
// 仅作为高层次示例参考。
import { useMyStore } from 'stores/myStore'
preFetch ({ store, redirect }) {
const myStore = useMyStore() // SSR 模式下使用 useMyStore(store)
if (!myStore.isAuthenticated) {
redirect({ path: '/login' })
}
}默认情况下,重定向会返回 302 状态码,但我们可以将状态码作为第二个可选参数传入,像这样:
redirect({ path: "/moved-permanently" }, 301);如果调用了 redirect(false)(仅在客户端支持!),则会中止当前的路由导航。请注意,如果在 src/App.vue 中这样使用,将会阻止应用的启动,这是不可取的。
redirect() 方法需要传入一个 Vue Router 的 location 对象。
使用 preFetch 来初始化 Pinia
preFetch 钩子在应用启动时只会运行一次,所以您可以借此机会来初始化 Pinia store。
// App.vue - 处理 Pinia stores
// 以一个名为 "myStore" 的 store 为例
// 位于 /src/stores/myStore.js|ts
import { useMyStore } from 'stores/myStore'
export default {
// ...
preFetch () {
const myStore = useMyStore()
// 对 myStore 执行一些操作
}
}加载状态
一个好的用户体验应当在后台工作进行时告知用户正在加载中,让用户耐心等待页面就绪。Quasar CLI 为此提供了两种开箱即用的方案。
LoadingBar
当您为应用添加了 Quasar 的 LoadingBar 插件时,Quasar CLI 默认会在 preFetch 钩子运行期间自动显示加载进度条。
Loading
也可以使用 Quasar 的 Loading 插件。示例如下:
import { Loading } from "quasar";
export default {
// ...
preFetch(
{
/* ... */
},
) {
Loading.show();
return new Promise((resolve) => {
// 在这里执行异步操作
// 然后调用 "resolve()"
}).then(() => {
Loading.hide();
});
},
};