为什么要捐赠
API 资源管理器
升级指南
NEW!
quasar.config 文件
将项目转换为使用 Vite 的 CLI
浏览器兼容性
支持 TypeScript
目录结构
命令列表
CSS 预处理器
路由
延迟加载 - 代码分割
处理资源
引导文件
预取功能
API 代理
处理 Vite
处理 process.env
使用 Pinia 进行状态管理
使用 Vuex 进行状态管理
代码风格检查工具
测试与安全审计
开发移动应用
Ajax 请求
向公众开放开发服务器
Quasar CLI 与 Vite - @quasar/app-vite
应用 Vuex 商店

警告

Vue 团队已弃用 Vuex,转而使用 Pinia

在大型应用程序中,由于多个状态片段分散在许多组件中以及它们之间的交互,状态管理通常会变得很复杂。通常会忽略 Vue 实例中真相的来源是原始数据对象 - Vue 实例只是代理对它的访问。因此,如果您有一部分状态应该由多个实例共享,您应该避免重复它并通过标识来共享它。

如果您希望组件共享状态,推荐的方式是使用 Vuex。在深入研究之前,请查看其 文档。当与 Vue 开发者工具 浏览器扩展(如时间旅行调试)一起使用时,它具有一个很棒的功能。

我们不会详细介绍如何配置或使用 Vuex,因为它有很棒的文档。相反,我们将只向您展示在 Quasar 项目中使用它时的文件夹结构。

index.js
# Vuex 商店定义
<folder>
# Vuex 商店模块...
<folder>
# Vuex 商店模块...

默认情况下,如果您在使用 Quasar CLI 创建项目文件夹时选择使用 Vuex,它将帮助您设置使用 Vuex 模块。 /src/store 的每个子文件夹都代表一个 Vuex 模块。

如果您在创建项目时没有选择 Vuex 选项,但希望稍后添加它,那么您需要做的就是检查下一部分并创建 src/store/index.js 文件。

提示

如果 Vuex 模块对于您的网站应用程序来说太复杂,您可以更改 /src/store/index.js 并避免导入任何模块。

添加 Vuex 模块。

通过 $ quasar new 命令,Quasar CLI 使添加 Vuex 模块变得容易。

$ quasar new store <store_name> [--format ts]

它将在 /src/store 中创建一个名为“store_name”的文件夹,该文件夹来自上面的命令。它将包含您需要的所有样板代码。

假设您想创建一个名为“showcase”的 Vuex 模块。您执行 $ quasar new store showcase。然后您会注意到新创建的 /src/store/showcase 文件夹,其中包含以下文件

index.js
# Vuex 商店定义
index.js
# 将模块粘合在一起
actions.js
# 模块操作
getters.js
# 模块获取器
mutations.js
# 模块变异
state.js
# 模块状态

我们已经创建了新的 Vuex 模块,但我们还没有通知 Vuex 使用它。因此,我们编辑 /src/store/index.js 并添加对它的引用

import { createStore } from 'vuex'
import showcase from './showcase'

export default function (/* { ssrContext } */) {
  const Store = createStore({
    modules: {
      showcase
    },

    // enable strict mode (adds overhead!)
    // for dev mode and --debug builds only
    strict: process.env.DEBUGGING
  })

  return Store
}

提示

如果您正在开发 SSR 应用程序,那么您可以查看 ssrContext 对象,该对象在服务器端提供。

现在我们可以在 Vue 文件中使用这个 Vuex 模块。这是一个快速示例。假设我们在状态中配置了 drawerState 并添加了 updateDrawerState 变异。

/src/store/showcase/mutations.js

export const updateDrawerState = (state, opened) => {
  state.drawerState = opened
}

// src/store/showcase/state.js
// Always use a function to return state if you use SSR
export default function () {
  return {
    drawerState: true
  }
}

在 Vue 文件中

<template>
  <div>
    <q-toggle v-model="drawerState" />
  </div>
</template>

<script>
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup () {
    const $store = useStore()

    const drawerState = computed({
      get: () => $store.state.showcase.drawerState,
      set: val => {
        $store.commit('showcase/updateDrawerState', val)
      }
    })

    return {
      drawerState
    }
  }
}
</script>

TypeScript 支持

如果您在使用 Quasar CLI 创建项目文件夹时选择使用 Vuex 和 TypeScript,它会在 src/store/index.ts 中添加一些类型代码。为了在您的组件中获得类型化的 Vuex 商店,您需要像这样修改您的 Vue 文件

<template>
  <!-- ... -->
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from 'src/store';

export default defineComponent({
  setup () {
    const $store = useStore();
    // You can use the $store, example: $store.state.someStoreModule.someData
  },
});
</script>

警告

使用 Vuex,目前只对状态进行了强类型化。如果您想使用类型化的获取器/变异/操作,您将需要使用 Vuex 之上的额外包或 Vuex 的替代品。

使用 Vuex 智能模块

完全类型化商店的选项之一是名为 vuex-smart-module 的包。您可以通过运行以下命令添加此包


$ yarn add vuex-smart-module

安装完成后,您需要编辑您的 src/store/index.ts 文件以使用此包来创建商店。编辑您的商店索引文件以类似于以下内容

import { store } from 'quasar/wrappers';
import {
  createStore,
  Module,
  createComposable,
  Getters,
  Mutations,
} from 'vuex-smart-module';

class RootState {
  count = 1;
}

class RootGetters extends Getters<RootState> {
  get count () {
    return this.state.count;
  }

  multiply (multiplier: number) {
    return this.state.count * multiplier;
  }
}

class RootMutations extends Mutations<RootState> {
  add (payload: number) {
    this.state.count += payload;
  }
}

// This is the config of the root module
// You can define a root state/getters/mutations/actions here
// Or do everything in separate modules
const rootConfig = {
  state: RootState,
  getters: RootGetters,
  mutations: RootMutations,
  modules: {
    //
  },
};

export const root = new Module(rootConfig);

export default store(function (/* { ssrContext } */) {
  const rootStore = createStore(root, {
    strict: !!process.env.DEBUGGING,
    // plugins: []
    // and other options, normally passed to Vuex `createStore`
  });

  return rootStore;
});

export const useStore = createComposable(root);

您可以像使用普通 Vuex 一样使用模块,并且在该模块中,您可以选择将所有内容放在一个文件中,或者为状态、获取器、变异和操作使用单独的文件。或者,当然,这两种方法的组合。

只需在 src/store/index.ts 中导入模块并将其添加到您的 rootConfig 中。例如,请查看 这里

在 Vue 文件中使用类型化商店非常简单,这是一个示例

<template>
  <q-page class="column items-center justify-center">
    <q-btn @click="store.mutations.add(3)" label="Add count" />
    <div>Count: {{ store.getters.count }}</div>
    <div>Multiply(5): {{ store.getters.multiply(5) }}</div>
  </q-page>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { useStore, root } from 'src/store';

export default defineComponent({
  name: 'PageIndex',
  setup() {
    const store = useStore();
    return { store };
  },
});
</script>

在引导文件中使用类型化商店

在引导文件中使用商店时,也可以使用类型化商店。这是一个非常简单的引导文件的示例

import { boot } from 'quasar/wrappers'
import { root } from 'src/store'

export default boot(({ store }) => {
  root.context(store).mutations.add(5);
});

在预取中使用类型化商店

同样,您也可以在使用 预取功能 时使用类型化存储。以下是一个示例

<script lang="ts">
import { defineComponent } from 'vue';
import { root } from 'src/store';

export default defineComponent({
  name: 'PageIndex',
  preFetch ({ store }) {
    root.context(store).mutations.add(5);
  },
  setup () {
    //
  },
});
</script>

存储代码拆分

您可以利用 预取功能 来拆分 Vuex 模块的代码。

拆分 Vuex 智能模块代码

与普通的 Vuex 相比,使用 Vuex 智能模块进行代码拆分的工作方式略有不同。

假设我们有以下模块示例

/store/modules/index.ts

// simple module example, with everything in one file
import { Getters, Mutations, Actions, Module, createComposable } from 'vuex-smart-module';

class ModuleState { greeting = 'Hello'};

class ModuleGetters extends Getters<ModuleState> {
  get greeting () {
    return this.state.greeting;
  }
}

class ModuleMutations extends Mutations<ModuleState> {
  morning () {
    this.state.greeting = 'Good morning!';
  }
}

class ModuleActions extends Actions<ModuleState, ModuleGetters, ModuleMutations, ModuleActions> {
  waitForIt (payload: number) {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        this.commit('morning');
        resolve();
      }, payload);
    })
  }
}

export const admin = new Module({
  state: ModuleState,
  getters: ModuleGetters,
  mutations: ModuleMutations,
  actions: ModuleActions,
});

export const useAdmin = createComposable(admin);

然后,我们希望仅在访问某个特定路由组件时加载此模块。我们可以通过(至少)两种不同的方式来实现。

第一种方法是使用 Quasar 提供的 预取功能,与普通 Vuex 的示例类似,可以在 此处找到。为此,我们在我们的 router/routes.ts 文件中定义了一个路由。在这个例子中,我们有一个 /admin 路由,它是我们 MainLayout 的子路由

{ path: 'admin', component: () => import('pages/Admin.vue') }

我们的 Admin.vue 文件如下所示

<template>
  <q-page class="column items-center justify-center">
    {{ greeting }}
    <q-btn to="/" label="Home" />
  </q-page>
</template>

<script lang="ts">
import { defineComponent, onUnmounted } from 'vue';
import { registerModule, unregisterModule } from 'vuex-smart-module';
import { admin, useAdmin } from 'src/store/module';
import { useStore } from 'vuex';

export default defineComponent({
  name: 'PageIndex',
  preFetch ({ store }) {
    if (!store.hasModule('admin')) {
      registerModule(store, 'admin', 'admin/', admin);
    }
  },
  setup () {
    const $store = useStore()
    // eslint-disable-next-line
    if (!process.env.SERVER && !$store.hasModule('admin') && (window as any).__INITIAL_STATE__) {
      // This works both for SSR and SPA
      registerModule($store, ['admin'], 'admin/', admin, {
        preserveState: true,
      });
    }

    const adminStore = useAdmin();
    const greeting = adminStore.getters.greeting;

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-expect-error
    // eslint-disable-next-line
    if (module.hot) {
      module.hot.accept(['src/store/module'], () => {
        // This is necessary to prevent errors when this module is hot reloaded
        unregisterModule($store, admin);
        registerModule($store, ['admin'], 'admin/', admin, {
          preserveState: true,
        });
      });
    }

    onUnmounted(() => {
      unregisterModule($store, admin);
    });

    return { greeting };
  },
});
</script>

第二种方法是使用 router.beforeEach 钩子来注册/注销我们的动态存储模块。如果您有应用程序的一部分,它只被一小部分访问者使用,那么这样做是有意义的。例如,您的网站上的 /admin 部分,在该部分下您有多个子路由。然后,您可以在路由导航时检查路由是否以 /admin 开头,并根据此情况为每个以 /admin/... 开头的路由加载存储模块。

为此,您可以在 Quasar 中使用一个 启动文件,如下所示

提示

下面的示例旨在同时适用于 SSR 和 SPA。如果您只使用 SPA,可以通过删除 registerModule 的最后一个参数来简化它。

import { boot } from 'quasar/wrappers';
import { admin } from 'src/store/module';
import { registerModule, unregisterModule } from 'vuex-smart-module';

// If you have never run your app in SSR mode, the ssrContext parameter will be untyped,
// Either remove the argument or run the project in SSR mode once to generate the SSR store flag
export default boot(({ store, router, ssrContext }) => {
  router.beforeEach((to, from, next) => {
    if (to.fullPath.startsWith('/admin')) {
      if (!store.hasModule('admin')) {
        registerModule(store, ['admin'], 'admin/', admin, {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error
          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
          preserveState: !ssrContext && !from.matched.length && Boolean(window.__INITIAL_STATE__),
        })
      }
    } else if (store.hasModule('admin')) {
      unregisterModule(store, admin);
    }

    next();
  });
});

在您的组件中,您可以直接使用动态模块,而无需担心注册它。例如

<template>
  <q-page class="column items-center justify-center">
    {{ greeting }}
    <q-btn to="/" label="Home" />
  </q-page>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { useAdmin } from 'src/store/module';

export default defineComponent({
  name: 'PageIndex',
  setup() {
    const adminStore = useAdmin();
    const greeting = adminStore.getters.greeting;

    return { greeting };
  }
});
</script>

在 Vuex 存储中访问路由器

只需在 Vuex 存储中使用 this.$router 即可访问路由器。

以下是一个示例

export function whateverAction (state) {
  this.$router.push('...')
}