vuex4 都 beta 了,vuex5 还会远吗?

随着vue3的出现,vue社区的其他核心包都进行了对于vue3的升级支持,例如vue-routervuex等。那么vuex有了哪些变化呢?今天我们一起来聊聊vuex

我们知道vue3比较核心的两个升级点:composition-api和 更好的 typescript 支持。vuex4呢? vuex4 更新了这些东西:

  • 对于vue3 的支持
  • 新的安装方式
  • 稍微好一点的 typescript 支持
  • 直接在vuex里面集成Logger插件

我们先一起来体验一下vuex4吧。

vuex4 + vue3 的基本使用

我们试着来创建一个 vue3 + vuex4 的项目:

这里使用 vite 的方式来创建

yarn create @vitejs/app
cd my-vite-app
yarn
yarn vuex@next

我们得到如下目录:

➜
.src
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
└── main.ts

我们来使用一下vuex4: 在src目录下新建store目录,在store下面新建 index.ts

import { createStore, createLogger } from "vuex"
import config from "../config"

const store = createStore({
  state: {},
  mutations: {},
  actions: {},
  strict: config.isDev,
  plugins: config.isDev ? [createLogger()] : []
})

export default store

vuex4 为我们提供了 createStore方法,通过该方法,我们可以创建一个store,并将其暴露出去,在main.ts里面引用它:

import { createApp } from "vue"
import store from "./store"
import router from "./router"
import App from "./App.vue"

const app = createApp(App)

app.use(store)
app.use(router)
app.mount("#app")

这里和 vue2 变得有些不一样了,vue3都是通过app.usestore注入。

添加 modules

一般来说我们不经常直接在根state创建变量,所以这时候我们想到了module的方式,在store目录下面创建modules目录,在 modules 目录下面创建homeabout 两个目录,分别创建state并导出:

// store/modules/home.index.ts
export interface HomeState {
  homeInfo: string
}

const state: HomeState = {
  homeInfo: "info from home state model"
}
const getters = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}
// store/modules/about.index.ts
export interface AboutState {
  aboutInfo: string
}

const state: AboutState = {
  aboutInfo: "info from about state model"
}
const getters = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

我们在modules目录下面再创建一个index.ts,将这些module一起合并导出:

// store/modules/index.ts
import home from "./home"
import about from "./about"

const modules = {
  home,
  about
}
console.log("modules", modules)

export default modules

接着vuex4为我们提供了 useStore 方法来获取state

本篇文章主要基于composition-api,所以暂不介绍mapStatemapGettersmapActions等的用法

我们试着来用一下:

<template>
	<div class="home">
		<img alt="Vue logo" src="../assets/logo.png" />
		<p>{{ homeInfo }}</p>
	</div>
</template>

<script lang="ts">
import { useStore } from 'vuex'
import { computed,defineComponent } from 'vue'
import HelloWorld from '../components/HelloWorld.vue'
import styles from './example.module.css'

export default defineComponent({
	name: 'Home',
	setup() {
		const store = useStore()
		const homeInfo = computed(() => store.state.home.homeInfo)

		return {
			homeInfo
		}
	}
})
</script>

到目前为止,我们已经实现了配置vuex4+ vue3 的基本使用.假设现在需要在home.vue取很多个 state 里面的数据呢?那么就会变成这样的:

...
const store = useStore()
const homeInfo = computed(() => store.state.home.homeInfo)
const value1 = computed(() => store.state.home.value1)
const value2 = computed(() => store.state.home.value2)
const value3 = computed(() => store.state.home.value3)
...

貌似重复代码很多,对不对?我们可以自定义一个hooks来代替这些重复操作:

import { computed } from "vue"
import { useStore } from "vuex"

const useVuexValue = (moduleName: string, storeKeys: Array<string>) => {
  let values: any = []
  const moduleNames = moduleName.split("/")
  const state = useCurry(moduleNames)
  storeKeys.forEach((storeKey) => {
    const value = computed(() => state[storeKey])
    values.push(value ? value : null)
  })
  return values
}

const useCurry = (moduleNames: Array<string>) => {
  const store = useStore()
  let state = store.state
  moduleNames.forEach((moduleName) => {
    state = state[moduleName]
  })
  return state
}

export default useVuexValue

然后我们取state变量的方法就变成了:

import { useVuexValue } from '../hooks'

...
setup() {
    const [homeInfo,value1, value2] = useVuexValue('home', ['homeInfo','value1', 'value2'])
    return {
        value1,
        value2,
        homeInfo
    }
}
...

假如home module 下面还有detaillist 等等子module,那我们取数据的方式就应该是:

...
setup() {
    const [value1, value2] = useVuexValue('home/detail', ['value1', 'value2'])
    return {
        value1,
        value2
    }
}
...

是不是看上去有点眼熟,对的就是类似于mapState的方式,不过是我们自定义的方式,同样的思路,可以封装我们自己的mutationaction等。

可能你阅读的时候注意到了,在定义每个module时,我们都声明了interface,我们都知道这是typescript的类型定义。但是我们用到了吗:

看到这个any就知道,咱们压根没法通过typrscript 来智能化提示咱们的state,那有没有方法呢?根据上面 👆 截图可知 (property) Store<any>.state: anyvuex4给我们提供了一个Store<any>.state.any,我们想办法把这两个any变成typescript类型就行了。

结合 typescript

按照官方的教程,咱们可以把typecriptvuex4结合起来,首先来改造一下store/index.ts:

// store/index.ts
import { InjectionKey } from "vue"
import { createStore, createLogger, Store, useStore as baseUseStore } from "vuex"
import modules from "./modules"
import config from "../config"

interface StateType {}

export const key: InjectionKey<Store<StateType>> = Symbol()

const store: Store<StateType> = createStore({
  modules,
  mutations: {},
  actions: {},
  strict: config.isDev,
  plugins: config.isDev ? [createLogger()] : []
})

export function useStore() {
  return baseUseStore(key)
}
export default store

通过export const key: InjectionKey<Store<StateType>> = Symbol() ,我们可以给咱们的stroe添加类型定义.对于这个 StateType ,咱们得想办法将modules 下面的homeabout的类型定义都给囊括进来,但是一般来说,我们的module 一般来说都比较多,所以我们不直接在index.ts定义StateType。我们应该把StateType的定义抽出来,作为types,所以我们在 src 目录下新建一个types 目录,然后新建一个index.ts:

// types/index.ts
import { HomeState } from "../store/modules/home"
import { AboutState } from "../store/modules/about"

type VuexModuleType = {
  home: HomeState
  about: AboutState
}

export type StateType = VuexModuleType

在这里,我们把各个module的类型文件都导进来了,然后一起合并导出,在store/index.ts下面,我们就能取到这里的StateType:

import { StateType } from '../types'
...
export const key: InjectionKey<Store<StateType>> = Symbol()

const store: Store<StateType> = createStore({
	modules,
	mutations: {},
	actions: {},
	strict: config.isDev,
	plugins: config.isDev ? [createLogger()] : []
})
....

接下来,我们在src下面新建一个vuex.d.ts:

import { ComponentCustomProperties } from "vue"
import { StateType } from "./types"
import { Store } from "vuex"

declare module "@vue/runtime-core" {
  // provide typings for `this.$store`
  interface ComponentCustomProperties {
    $store: Store<StateType>
  }
}

现在就剩最后一步了,我们在main.ts里面拿到store暴露出来的key:

// main.ts
import { createApp } from "vue"
import store, { key } from "./store"
import router from "./router"
import App from "./App.vue"

const app = createApp(App)

app.use(router)
app.use(store, key)
app.mount("#app")

现在,我们再来试一试呢?

我们成功了,现在我们的state都能智能提示了。貌似很完美了,对吗?但是事实上,当我们在一个module下面还有子module的时候,这种方法就不可行了,例如我们在modules/home下面再增加一个子module: list

// store/modules/home/list/index.ts
export interface HomeListState {
  homeList: string
}

const state: HomeListState = {
  homeList: "list form home list state model"
}
const getters = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

然后在home/index.ts导入:

// moudles/home/index.ts
import list from "./list"
const getters = {}
const mutations = {}
const actions = {}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
  modules: {
    list
  }
}

然而我们使用的时候却出现了问题:

有的同学说,你得把list的类型定义(HomeListState) 加到HomeState里面啊,我们来试一试:

然而结果并不像想象之中那么好。甚至vuex的官方仓库也有类似的问题:how to use modules in vuex4.0

那为什么会导致这种结果呢?是因为对于父module来说,子module的类型是dynamic的,dynamic的意思是说,动态的、不确定的。那我们如何避免这个问题呢?目前来说,我们只有将子module统一提至根module,意思就是不要在module里面嵌套module。只能说,这太遗憾了。除此之外,InjectionKey<Store<StateType>> = Symbol()这种方式,给人的第一直觉,就是有一种强行适配typescript的不适感。基于上面这些原因,vuex5会完全重写,以获得完整的typescript支持。

vuex5 新特性

从最新一次Vue.js Global Online Conference 中,我们可以知道,虽然vuex4还处于beta阶段,可是vuex5已经提上了日程。根据 vuex 核心贡献者:Kia King 所说,vuex5 将有以下特性:

  • 同时支持 Option APIComposition API
  • 完整的TypeScript支持
  • 舍弃Mutations,只有Actions
  • 舍弃nested modules,替换成 Compose
  • 自动Code splitting

我们来看看 vuex5 的变化吧。

Option API

我们先来看看如何定义一个基于option apistore

import { defineStore } from "vuex"
const countStore = defineStore({
  name: "couter"
})

首先,创建store的方式变了,由createStore变成defineStore,其中会包括一个name 作为identify。 接下来我们需要创建一些state:

import { defineStore } from "vuex"
const countStore = defineStore({
  name: "couter",
  state() {
    return {
      count: 1
    }
  }
})

接下来我们再创建一个getters:

import { defineStore } from "vuex"
const countStore = defineStore({
  name: "couter",
  state() {
    return {
      count: 1
    }
  },
  getters: {
    double() {
      return this.count * 2
    }
  }
})

当然还有 actions:

import { defineStore } from "vuex"
const countStore = defineStore({
  name: "couter",
  state() {
    return {
      count: 1
    }
  },
  getters: {
    double() {
      return this.count * 2
    }
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

store创建好了之后,接下来我们看看如何使用它:

import { createVuex } from "vuex"
import countStore from "./countStore"

const vuex = createVuex()
const counter = Vuex.store(countStore)

// counter.count -> 1
// counter.double -> 2
// counter.increment() -> Increment
// counter.count -> 2
// counter.double -> 4

通过以上这种方式,所有的state变量都会获得完整的typescript类型定义,你不需要自己定义类似泛型interface ,你就能获得他们的类型定义。刚刚我们在vuex4里面遇到的问题,全部都解决了。

接下来我们看看怎么在vue里面使用vuex5,首先还是一样的,我们应该在main.ts里面导入它,并将其注入到app里面:

import { createApp } from "vue"
import { createVuex } from "vuex"
import App from "./app.vue"

const app = createApp()
const vuex = createVuex()

app.use(vuex)
app.mount("#el")

接着在我们想要使用的地方:

<template>
	<div class="home">
		<p>count is:{{ couter.count }}</p>
        <p>double is:{{ couter.double }}</p>
        <button @click="counter.increment">
          Increment
        </button>
	</div>
</template>

<script lang="ts">
import { computed,defineComponent } from 'vue'
import counterStore from './counterStore'

export default defineComponent({
    name: 'Home',
    computed: {
        ...mapStore({
            counter: counterStore
        })
    }
})
</script>

vuex5提供了mapStore的钩子来获取state。在这里mapStore其实是this.$vuex.store(counterStore)的语法糖。

神奇的地方来了:

我们会发现,store的定义和vue组件的定义 几乎长得一模一样。这样做有什么好处呢?这样做,我们就能很方便的将vue组件的逻辑搬到vuex里面。同时也能保证开发的时候,组件vuex的逻辑保持高度一致性。

接下来我们再来看看composition-api

Composition API

import { ref, computed } from "vue"
import { defineStore } from "vuex"

const counterSore = defineStore("counter", () => {
  const count = ref(1)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return {
    count,
    double,
    increment
  }
})

你是不是又觉得有点眼熟,这玩意长得和现在vue3-composition-api组件的定义方式几乎一模一样。这里也是用了defineStore来定义一个Store,和option-api 不一样的是,这里用了一个callback而非一个Object。在这个callback Function 里面,我们可以使用到vue reactivity所包含的所有特性。同时我们也不需要预先去定义stategettersactions。在这里我们只是利用ref创建了一个变量,利用computed创建了一个计算属性的变量,同时也只是定义一个了一个简单的increment 函数去变更count

基于这样的机制,我们就可以使用vue3里面的所有其他特性,类似reactivewatch等等。相当于这完全取决于我们的自由组合,将hooks的思想放大到最大化。同时这样的方式,对于typescript来说,所有的东西都能获得完整的类型定义,终于完整地支持typescript了!!

接着还是来看看如何在vue组件中使用:

<template>
	<div class="home">
		<p>count is:{{ couter.count }}</p>
        <p>double is:{{ couter.double }}</p>
        <button @click="counter.increment">
          Increment
        </button>
	</div>
</template>

<script lang="ts">
import { computed,defineComponent } from 'vue'
import { useStore } from 'vuex'
import counterStore from './counterStore'

export default defineComponent({
    name: 'Home',
    setup () {
        const counter = useStore(counterStore)

        return {
            counter
        }
    }
})
</script>

一切都看上去那么完美。但是还有一个问题,vuex5里面没有modules,怎么办?

Store Composition

由于没有了modulesvuex5给我们提供了Store Compostion:

import { defineStore } from "vuex"
const greatStore = defineStore({
  name: "great",
  state() {
    return {
      info: "hello"
    }
  }
})

然后我们在另外的store里面如何获取呢:

import { ref, computed } from "vue"
import { defineStore } from "vuex"
import greatStore from "./greatStore"

const counterSore = defineStore("counter", ({ use }) => {
  const great = use(greatStore)
  const count = ref(1)
  const double = computed(() => count.value * 2)

  const countWithGreate = computed(() => {
    return `${great.info}-${count.value}`
  })

  function increment() {
    count.value++
  }

  return {
    count,
    double,
    increment
  }
})

vuex5给我们提供了use函数来获取其他state。只能说,简直太完美了,通过Compose的方式,我们可以在任何store里面组合其他store,从而达到我们自己想要的效果。

实现我们自己的状态管理

通过上述vuex5的新的特性,我们发现,其实vuex5是很好的利用了composition-api的思路,结合vue3 reactivity的特性,达到了这样的效果。基于这种思路,其实我们完全可以创建自己的状态管理耶,对不对?就像hooks出来之后,很多react 开发者,都放弃了redux,直接在项目里面结合useReducer实现状态管理。那在vue3里面,我们是不是也可以呢?我们一起来试一试吧。

第一步,我们结合reactive来定义我们的state:

// custom_store/info/state.ts
import { reactive } from "vue"

export interface infoState {
  info: string
}

const state: infoState = {
  info: "info from info state model"
}

export const createStore = () => {
  return reactive(state)
}

好了创建完了,可以使用了。如果我只说到这,你可能会顺着网线过来锤我,别着急啊,我们一步一步来。 上面我们创建了一个state,这里我们用到了reactive,是因为我们利用vue3-reactivity的机制来保证咱们创建的state变量是可响应式的。目前为止,我们只是定义了state

接下来,我们来定义一些改变state的方法,即action:

// custom_store/info/action.ts
import { infoState } from "./state"

function chageInfo(state: infoState) {
  return (value: string) => {
    state.info = value
  }
}

export function createAction(state: infoState) {
  return {
    chageInfo: chageInfo(state)
  }
}

好了现在改变stateaction也有了。接下来我们来把他们组合起来,让它们看上去更像vuex一点:

import { readonly } from "vue"
import { createAction } from "./action"
import { createStore } from "./state"

const state = createStore()
const action = createAction(state)

export const useStore = () => {
  return {
    state: readonly(state),
    action: readonly(action)
  }
}

是不是有那味了。这里为什么要利用readonly呢?是因为一般来说,我们不能直接修改state的变量,只通过action去改变state的值,增加readonly,我们就完美的实现了这一点。

接下来我们在vue组件中使用一下:

<template>
	<div class="home">
		<img alt="Vue logo" src="../assets/logo.png" />
		<p>{{ info }}</p>
		<button @click="changeInfo">
			Click to change Info
		</button>
	</div>
</template>

<script lang="ts">
import { computed,defineComponent } from 'vue'
import { useStore } from '../custom_store/info'

export default defineComponent({
	name: 'Home',
	setup() {
		const store = useStore()
		const info = computed(() => store.state.info)

		function changeInfo() {
			store.action.chageInfo('hhh')
		}

		return {
			info,
			changeInfo
		}
	}
})
</script>

是不是完全和vuex5的使用方式一模一样,而且不知道你注意到没有,我们完美的实现了对typescript的支持,所有的类型都会被自动推断:

还有,其实我们也同时实现了store compose,我们只需在任意想要的store里面,导入其他任意的store即可。同时我们也支持其他的vue3的钩子,你完全可以自由组合。剩下的东西,留给小伙伴们自己探索咯。

说到最后

可以说,react hooksvue3 composition-api的出现。极大的变革了我们编程的思维方式,通过探讨vuex的今生后世,我们能够很清晰到认识到函数式编程的优点和前端的发展方向。这无疑是最近一年,前端最大的进步,结合typescript,从编程思想上,推动了前端的发展。话不用多说,各位小伙伴赶紧操作起来吧。

另外,如果想查看文章的示例代码,请点击这里:vue-vite-template.

题外话

同时也打个小广告。最近写了一个小玩意:monia-cli

  • 什么是 monia-cli?

    这是一款同时支持vue2vue3reactflutter的项目脚手架。

  • monia-cli 能做什么?

    1、每次新建项目,我们需要花大量的时间去新建一些重复的代码文件,例如request,config等等,“不会偷懒的程序员不是好程序员”。利用monia`,你完全可以省掉这些重复的工作。

    2、monia所提供的template都是目前最新的,最全的项目template。其中vue3template, monia 采用了最新的vue + vite + typescript的项目组织方式。fluttertemplate,会帮你省掉很多重复的工作,包括comon componts,comon utils等等。

    3、monia本身还支持错误提示,命令联想等功能。

    4、使用示例:monia create test_flutter_app