Vue3 - 新的状态管理方式

Vue.js
Vue3 中是否仍然需要 Vue Store?对我来说答案是 否定 的!原因如下所示。

为何不应该使用 Vuex

如果还不了解 Vuex ,可以在此找到更多相关信息:vuex.vuejs.org/

简单来说,Vuex 是 Vue 的状态管理插件。提供了开箱即用的响应式功能,并且它很好的集成到了 Vue 开发者工具中。

在广泛使用 Vuex 后,我不得不说它真的不讨我喜欢。因为在模块代码中会包含一些不必要的模板和结构。

大多数人想通过 Vuex 实现什么?

我认为最常见的用法是在组件间共享状态。此外,Vuex 还会确保应用的状态只能在程序的特定位置修改。

我认为,上述情况其实无需使用 Vuex。你可以简单的引入自己的存储机制而无需受到 Vuex 复杂性的限制和影响。

样板

看一下代码以便理解我的意思:

let state = {
    count: 0
}

let actions = {
    incrementCount({commit}) {
        commit('incrementCount')
    }
}

let mutations = {
    incrementCount(state) {
        state.count++
    }
}

export default {
    namespaced: true,
    state,
    mutations
}

现在看下我们实际需要的:

export const clickStore = {
    state: {
        count: 0
    },
    incrementCount() {
        clickStore.state.count++;
    }
}

随着存储状态的增长,问题只会越变越大,因为你总是需要去应对状态的变化和其行为。我的建议是,仅使用 actions 去使状态发生改变。其他所有的逻辑都应该拆分为独立的服务文件。

由于 Vuex 建议仅在 mutation 块儿中改变状态,因此你基本都会写成如上所述的样板代码。不过值得高兴的是,我们接下来将实现一种替代方案。啪的一下,很快啊 :)

代码结构

不幸的是,你依然需要在状态存储内处理一系列的字符串。并且如果你有后端经验或者管理一些主要的代码库,你就会知道重构一个依赖于字符串的调用是一个多么痛苦的过程。

因此,提交一个行为或者更改存储总会触发带有字符串关键字的函数,就如下面这样:

incrementCount({commit, dispatch}) {
    commit('incrementCount')
    dispatch('someOtherAction')
}

相信你已经看到了重构这样的代码会有多少的麻烦,并且你可能永远不会更改这些变更操作和行为的名称,因为这意味着你的应用可能会被玩坏。

Vuex 的 Map 函数部分的解决了这个问题,详情请见:vuex.vuejs.org/guide/actions.html#...。不过重构依然是个问题。

不过你可能会问了,JS 在重构上不总会有各种问题么?

说的没错。这也是我推荐你使用 TypeScript 的原因之一。我曾试着仅使用 JS 构建一个企业级的软件,然后不出所料的失败了。我将会把详细情况整理为博客并发布到此主题下,并粘贴链接至此。

不过 Vuex 没有官方的 typescript 的实现,我们又迷茫了。我目前已知的唯一一个相关项目是 github.com/paleo/direct-vuex (一个超赞的开源项目!)并且作者在项目中也非常活跃,不过我个人是不会将其用作企业环境的。

因此,让我们一起看下如何使用 TypeScript 实现状态存储,并且无需那些无用的模板代码 :)

Vue 3 救急

所以就像我们指出来的那样,你在 Vue3 中再也无需使用 Vuex 状态存储了。实现自己的状态存储非常的简单,并且比 Vuex 的更加灵活且更加可维护。需要注意的是,下面的示例将使用 TypeScript 编码。如果你想看看与之匹配的普通 js 文件,请访问:github.com/Mario-Brendel/Vue3-Test...

首先,确定下我们的目标,我不想要一个全局状态(虽然这更容易实现) - 而且,我还打算在应用的不同部分对应有不同的状态存储。理想情况下,应用中的所有的模块都应该是独立的(状态存储也应如此)。

存储

我想先封装一下 Vue 响应式的逻辑。并且让开发人员可以不用考虑这部分的实现方式。接下来看下如何实现:

import {reactive, readonly} from 'vue';


export abstract class Store<T extends Object> {
    protected state: T;

    constructor() {
        let data = this.data();
        this.setup(data);
        this.state = reactive(data) as T;
    }

    protected abstract data(): T

    protected setup(data: T): void {}

    public getState(): T {
        return readonly(this.state) as T
    }
}

首先声明了一个继承自 Object 类的 Storage 类。这样做可以保证只有对象会被用于这个状态。下面你也会领略到我这样做的用意。
接下来,看下初始化了响应式数据的构造函数(这就是我想封装的那部分代码)。

看看这行代码:

this.state = reactive(data) as T;

这就是魔法!!!Vue3 的响应方法让我们的对象(来自抽象的 data() 函数) 具备响应式能力。除此之外,我们还添加了 as T 这一部分。这可以确保没有人可以知道我们处理的是代理对象并且在里边完整的执行了正确的代码。:)

另外,我们的抽象类有着这三个方法

protected abstract data(): T

protected setup(data: T): void {}

public getState(): T{
    return readonly(this.state) as T
}

现在,你可以忽略前两个方法,不过 getState() 方法非常重要。这里我们将会用到源自 Vue3 的 readonly 函数。这样即可确保状态只能在该类内部被更改(还适用于数组和嵌套对象)。因此,其他所有的实例只能获取到我们对象的只读版本。如果你试着修改状态存储中的值,比如下面这个操作:

store.getState().test =123

你将会在开发控制台中收到警告,并且这个操作会被忽略。

这就是我喜欢用 Vue3 的原因!!!使用 Vue2 你将无法使用这么强的功能,因为所有的响应式操作都与 Vue 实例紧密相连。并且,你看下上面的代码,不存在一个 Vue 实例 - 你可以直接获得响应式的能力。

存储实现

让我们开始实际的实现状态存储并且在某个地方用一用。为此,我创建了一个叫做 click-store.ts 的文件,这个文件可以获知给定按钮的点击次数(可以认为是给这个小博客中的功能做一个状态存储 vue-3-alpha-has-started )。

import {Store} from "./main";

interface Click extends Object {
    count: number
}

class ClickStore extends Store<Click> {
    protected data(): Click {
        return {
            count: 0
        };
    }

    incrementCount() {
        this.state.count++;
    }
}

export const clickStore: ClickStore = new ClickStore()

首先,我们提供了一个描述状态并将其用作存储类型的参数。如此一来,data() 函数就必须返回一个 Click 对象。在 data() 方法中,我们需要初始化必要的状态,然后 Store 父类将会使其支持响应式。

接下来,我们可以创建一个 incrementCount() 方法,并且不需要使用函数注入之类的手段 - 就像 Vuex 中的 commit 一样 - 调用一个其他函数从而改变状态。

。。。就是如此神奇。每个新的状态存储都只需挂在 data() 函数并且提供一些方法来修改状态即可。让我们看下在 Vue 文件中会是什么样的体现。

在 Vue 文件中使用状态存储

<template>
    <div>
        <button @click="inc()">Clicked {{ countState.count }} times.</button>
    </div>
</template>

<script>
    import {clickStore} from "../store/click-store";

    export default {
        setup() {
            const inc = () => {
                clickStore.incrementCount()

                // should throw a warning and don't mutate the store
                clickStore.getState().count++
            }

            return {
                countState: clickStore.getState(),
                inc
            }
        }
    }
</script>

如你所见,我创建了一个 inc() 函数,它首先调用了 incrementCount() 方法,这个方法由状态存储提供,然后尝试直接改变这个状态。第二个调用将不会有任何信息发生改变,因为我们使用 readonly 函数来阻止这一操作。

最后,我们通过返回值提供状态。请一定要记住,你需要在模板中提供状态。否则你将失去变量响应式的能力。因为这是状态对象的一部分,而不是一个独立的对象。比如你像下边这样写:

count: clickStore.getState().count,

那么存储数据的变更将不会在模板中注册。

总结

只需要短短几行代码,即可提供一个不错的状态存储解决方案,并且比 Vuex 存储更具灵活性也更易维护。此外,通过与 TypeScript 的结合,或得了类型检查和开箱即用的能力,让你的编码更加轻松 :)。

虽然看起来我不喜欢 Vuex,不过我依然认为有适合使用它的场景。。。不过对于大多数项目来说,我觉着不使用 Vuex 更好。

上述所有代码可于此处查看:github.com/Mario-Brendel/Vue3-Test...

关于这个话题如果你有任何的疑惑或者反馈,请留言或邮件 :)。

*[^_^]:翻译分段有误,回删

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@mario.brendel1990/vu...

译文地址:https://learnku.com/vuejs/t/52430

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 2

算了吧, vite+vue3.0 打包出来的东西居然不能在微信PC版的内置浏览器运行, vue3 浏览器是他的拌脚石, 还需要点时间让生态成熟起来才能让vite这个好家伙有价值, 那时就是 vite的天下了, 没webpack的事了

3年前 评论
wj2015 (楼主) 3年前

文章中 =======不过 getStage () 方法非常重要==========。 是getState还是手误写错了呢?

2年前 评论
wj2015 (楼主) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!