原文:https://medium.com/3yourmind/large-scale-vuex-application-structures-651e44863e2
作者: @Kevin Peters
在编写大型应用程序时,管理前端的状态可能非常困难。例如,对于Vue.js应用程序,有一个名为Vuex的插件,它以非常简单的方式提供状态管理,并建议使用以下应用程序结构:
如果你对案例感兴趣的话,可以查看官方Vuex案例中的购物车示例(vuejs/vuex—shopping-cart)或者我创建的示例(igeligel / vuex-simple-structure)。
这真的很有效,我们在这个模块中拥有包含了action,getter和mutation的简单的Vuex模块。共享的action,getter和mutation直接保存在store目录下。然后所有的组件,全局action,getter和mutation被导入index.js文件,并在Vuex模块的构造函数中再次导出。然而当有越来越多的组件的时候可能会出现问题,对于大型应用来说这是很常见的。想象一下像GitLab这样的应用程序,它包含了很多的模块。例如,GitLab的仓库侧边栏看起来像这样:
每个菜单入口基本上都是一个包含了多个action,getter和mutation的组件。全部这些部分被罗列在一个单模块文件中。这并不能很好地扩展,因为考虑到模块需要多少功能,甚至模块都可能变得非常大,从而导致模块拥有超过1000行代码。
但是这个问题是有解决办法的。我们可以提取module目录中的action、getter和mutation。全局action、getter或mutation可以直接存在于store目录中。应用程序结构如下:
基本上,你仍然有可能使用全局action、getter和mutation,但是我建议你这么做,因为它不是真正必需的。使用这种方法,我们将会有多个分离的文件。 chat模块中所有的action、getter和mutatioin将会由chat模块中的索引导入。 然后,此模块将导入到全局存储中。需要注意的是你应该在module中设置命名空间选项,以便具有正确的命名空间。 这在store / index.js文件中完成:
import Vue from 'vue';
import Vuex from 'vuex';
import chatModule from './modules/chat/index';
import productsModule from './modules/products/index';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
chat: chatModule,
products: productsModule,
},
});
在store中,我们拥有chat和products两个模块,它们两都拥有action、getter和mutations,而且都被导入到主模块文件index.js然后再次导出。最后导出数据可以被store模块使用。
这将注册模块,然后代码将会以一种可读、可导航、可维护的方式分离。关于这种执行方法的例子可以在bstavroulakis/vue-wordpress-pwa或者在我的示例igeligel/vuex-namespaced-module-structure中找到。这种应用程序结构可以很好的处理中小型应用。代码库的新开发人员不会很难找到业务逻辑所在的位置,因为每个模块在组件内部都拥有一个合理的名称和引用。使用模块真的很有趣,这在官方文档中有解释。
不过在某些时刻,存在一个问题。当你的后端团队添加更多的API,这个应用程序变得越来越复杂。当你拥有20,30甚至50个模块的时候,虽然仍然是可维护的,但是你的新开发人员会觉得很费劲,因为他不确定业务逻辑在哪里被调用。然后你会思考如何更好的的构建。你可能会直接在组件中调用API,但是这将会造成一个巨大的混乱,因为组件将持有业务逻辑。组件应该渲染数据而不是处理数据。
在React中有容器和组件的概念。Vue.js并没有强力执行。容器也是组件,他们都能从store获取数据并进行绘画。组件只是用来保存数据和渲染数据。他们通过props和上层容器进行通信。让我们设想一下。应用程序中的一个聊天小部件,它需要从存储中获取某种数据,甚至从API中获得更好的数据。我们将创建一个简单的示例,从聊天中获取所有消息,而不提供实时支持。让我们假设我们拥有某种容器能够保存整个chat。这个容器将会和store进行通信以更新数据,或者将数据填充到展示组件。这整个架构显示在以下的小图中。
在这个系统中,我们拥有一个叫做Chat.vue的容器,它和store模块chat通信。这个chat模块调用API和更新store处理逻辑。当state最终更新容器时,Chat.vue也会通过计算属性进行更新,该属性将根据Vue.js和Vuex的反应进行更新。在此之后,该属性将作为props传递给ChatList.vue。因为这个组件中的props是个数组,因此将进行一个迭代以渲染ChatListElement中的一个数组。Vue组件负责渲染聊天消息和元信息。
通过这种模式,我们把应用程序分成三个部分。一部分是存在于store模块中的业务逻辑,或者更一般地说是存在于store中。容器元素负责获取到数据并将数据填充到展示组件,展示组件只是用于渲染数据。这为我们提供了很好的模块化,并支持单一责任原则。它还提供了良好的可测试性,因为您可以自己测试这个结构的每个部分。它们一起会形成某种集成测试。但这可以在另一篇文章中讨论。
现在假设应用程序变大了很多,我的意思是你有很多模块,但是不清楚这些模块在哪里使用,哪些组件依赖它们,哪些不依赖它们。在大型应用程序中,这可能是一个真正的问题。想象一下,一个刚接触这个代码的人可以忽略50个模块和大约50个组件。他会有一个大问题要解决。
Vuex的建议是在store目录中包含业务逻辑特性的目录。有时,与使用这些模块的容器的连接可能会被破坏,而使用这些Vuex模块的地方就不清楚了。有些模块可能只是因为一个容器而存在,所以最好将这个业务逻辑放在容器附近,以便处理数据。让我们对应用程序进行一些重构。这个模板基于vuejb -templates/webpack。
唯一的区别是,我将Vuex安装到这个模板中,设置它并在src目录下添加modules目录。您可以在本文后面的文章中找到这个应用程序。这个目录的不同之处在于它包含模块。不要将这些模块与Vuex模块混合。可能有一个更好的名字,所以如果你知道,请在这篇文章下评论。在modules目录中,我们有这个Vue的模块。js应用程序。它看起来是这样的:
在modules目录中,有几个描述不同功能的目录。例如,我们有聊天和产品功能。但有趣的是,在那些modules目录中。我们有一个store目录,一个index.vue文件和组件。为了清楚起见,我们将只查看单个文件组件文件。index.vue用作容器组件。此容器将从store中提取所有数据,并将此数据作为props传递给组件。组件ChatList.vue和ChatListElement.vue就是从组件中获取数据并触发对存储的操作,该存储全局附加到Vue.js实例。最大的问题是为什么这些组件不在组件目录中。原因是这些组件是专门为此功能而制作的。如果它们已被重用于另一个功能,那么我会考虑将其移动到组件目录中。基本上这里的问题是,组件是否以某种方式重用。然后我们应该将组件重构到共享组件目录中。现在来说store。它与其他模式基本相同,但移入本地目录存储。要注册它,我们使用Vuex的registerModule函数。该函数将动态注册Vuex模块。通常它用于插件,但我们会在这里使用它来更好地分离关注点。在index.vue文件中,我们可以通过Vue.js访问生命周期函数,在创建的函数内部,我们可以安全地创建模块。
import { mapGetters } from 'vuex';
import store from './_store';
import ChatList from './_components/ChatList';
export default {
name: 'ChatModule',
components: {
ChatList,
},
computed: {
...mapGetters({
messages: '$_chat/messages',
}),
},
created() {
this.$store.registerModule('$_chat', store);
},
mounted() {
this.$store.dispatch('$_chat/getMessages');
},
};
我们在前面加上$ _来表示该模块是私有的,因为它只在模块中可用。注册之后,store将被填充到全局Vuex store中。之后我们便可以在组件内部使用这些Vuex函数。要注册store,我们需要某种方式把Vuex功能添加到Vue.js实例中。这可以通过创建空的Vuex store,导出它并将其附加到Vue.js构造函数来轻松完成。可以查看这些文件store/index.js, main.js获得灵感。
如果我们需要某个全局store,我会使用推荐的结构创建一个在store目录下的Vuex组件。比如我们需要在应用程序中的不同地方进行身份校验,那么最好以不与容器耦合的方式共享它。这是一个使用共享Vuex组件的很好的例子。
其中的一些缺陷:可能不清楚哪些模块是全局的,哪些模块是局部的,这真的很难决定。也很难找到全局组件,但是基本上,所有通用组件都应该在这个目录中,不同的模块使用这个目录。维护这个结构确实很困难,但是最后,我认为为了扩展应用程序,它是值得的。另一个陷阱是命名。现在到处都有组件目录。在模块_components中命名目录可能更好,以显示它们是私有组件,但这是个人偏好。
这种结构的一个很好的论据是模块在某种程度上是可提取的。 如果某个功能太大,你可以通过在src / modules目录下的目录中创建一个模块来提取它,然后从中创建一个npm包。 唯一需要导出的是容器组件。 然后,这个npm包可以在您公司的注册表中托管,也可以在npm上公开托管。 只需确保以某种方式使Vuex模块的行为可配置。 另一个好的论点是测试可以用特征范围的方式编写。
最好的结果是,每个阅读代码的开发人员都很清楚Vuex模块、容器和组件。你可以很快找到每一个功能的业务逻辑,并且功能很容易测试,因为在整个应用程序中使用了关注点分离的原则。
不同结构的例子:
- igeligel/vuex-simple-structure
- igeligel/vuex-namespaced-module-structure
- igeligel/vuex-feature-scoped-structure