Quasar CLI with Vite - @quasar/app-vite
预取(PreFetch)功能

预取是一项功能(仅在使用Quasar CLI时可用),它允许Vue路由(在/src/router/routes.js定义)获取的组件去:

  • 预取数据
  • 验证路由
  • 当某些条件不满足时(如用户未登录),重定向到另一条路由
  • 可以帮助初始化存储状态

以上所有内容都将在实际路由组件呈现之前运行。

它适用于所有Quasar模式(SPA、PWA、SSR、Cordova、Electron),但它对SSR构建特别有用。

安装

// quasar.conf.js
return {
  preFetch: true
}

WARNING

当你用它来预取数据时,你可能想使用Pinia或Vuex,所以当你创建项目时,确保你的项目文件夹有/src/stores(用于Pinia)/src/store(用于Vuex)文件夹,否则生成一个新项目并将store文件夹内容复制到当前项目(或使用quasar new store命令)

预取功能如何帮助SSR模式

此功能对SSR模式特别有用(但不仅限于此)。在SSR期间,我们基本上渲染了我们应用程序的“快照”,因此如果应用程序依赖于某些异步数据,那么在开始渲染过程之前,需要预先获取并解析此数据

另一个问题是在客户端上,在我们安装客户端应用程序之前需要提供相同的数据 - 否则客户端应用程序将使用不同的状态呈现,并且水化作用将失败。

为了解决这个问题,所获取的数据需要存在于视图组件之外,专用数据存储或“状态容器”中。在服务器上,我们可以在渲染之前预先获取数据并将数据填充到存储中。在我们安装应用程序之前,客户端存储将直接获取服务器状态。

当预取功能被激活时

preFetch钩子(在下一节中描述)由访问的路由决定 - 它也决定了渲染的组件。实际上,给定路由所需的数据也是在该路由上渲染的组件所需的数据。 因此将钩子逻辑仅置于路由组件内是很自然的(也是必需的)。 这包括/src/App.vue,在这种情况下,它只会在app启动时运行一次。

让我们举一个例子来了解何时调用钩子。假设我们有这些路由,并且我们为所有这些组件编写了preFetch钩子:

// routes
[
  {
    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然后登陆页面自我们的应用程序启动以来,就调用了App.vue挂钩。
/shop/allShopLayout然后ShopAll-
/shop/newShopNewShopNew是ShopLayout的子项,ShopLayout已经渲染,因此不再调用ShopLayout。
/shop/product/pyjamasShopProduct-
/shop/product/shoesShopProductQuasar注意到已经渲染了相同的组件,但是路由已经更新并且它有路由参数,所以它再次调用了钩子。
/shop/product/shoes/overviewShopProduct然后ShopProductOverviewShopProduct具有路由参数,因此即使已经渲染它也会被调用。
/登陆页面-

用法

钩子被定义为我们的路由组件上名为preFetch的自定义静态函数。请注意,因为在实例化组件之前将调用此函数,所以它无法访问this

下面是使用Vuex时的例子:

<!-- some .vue component used as route -->
<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>,那么就在它之外添加一个<script>部分,它只是用preFetch()方法返回一个对象:

<script>
export default {
  preFetch () {
    console.log('running preFetch')
  }
}
</script>


<script setup>....</script>

TIP

如果你正在开发一个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路由器位置对象。

使用预取功能初始化Pinia or Vuex Store(s)

当应用程序启动时,preFetch挂钩只运行一次,因此您可以利用此机会在此处初始化Pinia store(s) or the Vuex Store。

// -- Pinia on Non SSR --

// 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
  }
}
// -- Pinia on SSR --

// 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 ({ store }) {
    const myStore = useMyStore(store)
    // do something with myStore
  }
}
// App.vue - handling Vuex store

export default {
  // ...
  preFetch ({ store }) {
    // initialize something in store here
  }
}

Vuex存储代码拆分

在大型应用程序中,您的Vuex存储可能会分成多个模块。 当然,也可以将这些模块代码分割成相应的路由组件块。 假设我们有以下存储模块:

// src/store/foo.js
// 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()钩子中延迟注册这个模块:

// inside a route component
<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>

还要注意,因为模块现在是路由组件的依赖项,所以它将被Vite移动到路由组件的异步块中。

WARNING

不要忘记为registerModule使用preserveState: true选项,以便我们保持服务器注入的状态。

Vuex和TypeScript用法

您可以使用preFetch助手对存储参数进行类型提示(否则将具有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
  }),
}

TIP

这只对键入store参数有用,其他参数即使使用正常语法也会自动键入。

加载状态

一个好的用户体验包括在他/她等待页面准备好时通知用户在后台正在处理某些事情。 Quasar CLI为此提供了两种开箱即用的选项。

加载栏

当您向应用程序添加Quasar加载栏 插件时,Quasar CLI将在默认情况下运行preFetch挂钩时使用它。

加载中

还可以使用加载中 插件。 这是一个例子:

// a route .vue component
import { Loading } from 'quasar'

export default {
  // ...
  preFetch ({ /* ... */ }) {
    Loading.show()

    return new Promise(resolve => {
      // do something async here
      // then call "resolve()"
    }).then(() => {
      Loading.hide()
    })
  }
}