Quasar CLI with Webpack - @quasar/app-webpack
应用程序Vuex存储

WARNING

Vue团队放弃了Vuex,改用Pinia

在大型应用中,由于多块状态散落在许多组件中以及它们之间的相互作用,状态管理常常变得很复杂。人们常常忽略的是,Vue实例的真实来源是原始数据对象–Vue实例只是代理了对它的访问。因此,如果你有一块应该被多个实例共享的状态,你应该避免重复它,而是通过身份来共享它。

如果你想让组件共享状态,推荐的方法是Vuex。在深入研究之前,先看看它的文档。当与Vue dev-tools浏览器扩展一起使用时,它有一个伟大的功能,如时间旅行调试。

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

.
└── src/
    └── store/               # Vuex Store
        ├── index.js         # Vuex Store definition
        ├── <folder>         # Vuex Store Module...
        └── <folder>         # Vuex Store Module...

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

如果你在创建项目时没有选择Vuex选项,但想在以后添加它,那么你只需要查看下一节,并创建src/store/index.js文件。

TIP

如果Vuex模块对你的网站应用来说太多,你可以改变/src/store/index.js,避免导入任何模块。

添加一个Vuex模块。

Quasar CLI通过$ quasar new命令使添加Vuex模块变得简单。

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

它将在/src/store中创建一个文件夹,以上述命令中的 "store_name "命名。它将包含所有你需要的模板。

假设你想创建一个"showcase"Vuex模块。你发出$ quasar new store showcase。然后你注意到新创建的/src/store/showcase文件夹,它包含以下文件:

.
└── src/
    └── store/
        ├── index.js         # Vuex Store definition
        └── showcase         # Module "showcase"
            ├── index.js     # Gluing the module together
            ├── actions.js   # Module actions
            ├── getters.js   # Module getters
            ├── mutations.js # Module mutations
            └── state.js     # Module state

我们已经创建了新的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
}

TIP

如果你正在开发一个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>

WARNING

在Vuex中,目前只有状态是强类型的。如果你想使用类型化的getters/mutations/actions,你需要在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一样使用模块,在该模块中,你可以选择把所有东西放在一个文件中,或者为state, getters, mutations and actions使用单独的文件。当然,也可以是这两者的结合。

只要在src/store/index.ts中导入该模块并将其添加到你的rootConfig中。对于一个例子,请看这里

在Vue文件中使用类型化的存储是非常直接的,这里有一个例子:

<template>
    <q-page class="column items-center justify-center">
        <q-btn @click="store.mutations.add(3)">Add count</q-btn>
        <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>

在Boot文件中使用类型化的存储

当在 Boot 文件中使用存储时,也可以使用类型化的存储。下面是一个非常简单的引导文件的例子:

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

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

在Prefetch中使用一个类型化的存储

同样,在使用预取功能时,你也可以使用一个类型化的存储。下面是一个例子:

<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的例子,可以在这里找到(/quasar-cli-webpack/prefetch-feature#store-code-splitting)。为了做到这一点,我们在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="/">Home</q-btn>
    </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中使用一个启动文件,看起来像这样:

TIP

下面的例子被设计成同时适用于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="/">Home</q-btn>
    </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('...')
}