前端项目如何组织文件夹结构以及如何设计状态?

        今天我们来聊聊如何组织项目文件夹结构。很多人在做项目开发时应该都会有这样的感受:项目初期,进展快。但等功能做得多一些,会突然感觉进展慢下来了。在剩下的功能开发上花的时间可能越来越多,这就导致我们很难预估项目究竟什么时候才能做完。

        产生这个现象的原因是多种多样的,随着功能的增加,项目会变得越来越复杂。而在项目的后期,每增加一个新功能,或者修改已有的一些功能,都会带来很大的工作量。毕竟在做这些新的改动时,你需要考虑它们会对已有的功能产生什么影响。

        要解决这个问题,我们就要谈到软件工程,都知道软件工程设计要具备可扩展性,可维护性等,我们就会去合理架构一个软件工程。我给大家列举下常用的几种最常见的软件架构。如分层架构,微服务架构,微核架构等等。

        我先用微服务架构举例,相信大家对这种架构不陌生。 我也经常和其他人探讨过这个架构。 每个人对微服务都会有一些自己的见解。大家说的最多的就是把一个巨型服务分为一个个的单独小服务,然后再聚合起来。那么现在就来了一个问题,服务如何划分,根据什么来划分?

        服务如何划分确实是一个重点,有人说以技术来划分, 有些人说以业务来划分等等,为来弄明白这些问题,前几年我花了大量时间查阅一些资料和实践,最终选择DDD(领域驱动设计)来划分服务边界。

        后端微服务会用领域模型来划分业务边界, 那么我们前端是用什么来划分业务边界?毫无疑问,当然是领域驱动设计。

      我们弄明白以上问题,现在我们聊聊软件复杂度的根源 - 复杂的依赖关系

      如果我们做的项目多了,仔细思考就会发现,当某个功能需要层层嵌套的模块依赖,那么即使开发时觉得思路很顺,但是自己再回头去看,或者要让别人理解某个功能实现,就不得不去翻阅很深的调用链。这就是让你觉得复杂的直接原因。软件复杂度的根源完全来自复杂的依赖关系。找到了问题的根源,接下来就是解决问题了。在功能不断增加的时候,我们该怎么避免线性地产生复杂度呢?

      我们在寻找解决方案前,不妨考虑一下以下场景:
      场景一: 我们需要开发一个有 100 个页面的应用,功能很多。但是呢,每个页面都是独立的,所有功能都是在各自的页面内实现,彼此之间没有任何共享的模块。

        场景二: 我们需要开发一个有 100 个页面的应用,功能很多。大部分功能在各个页面中交叉引用,也有大部分组件存在很深的调用链。

      可以看到,场景一中,即使增加了新的页面,但事实上,也没有增加太多复杂度。而且,在修改已有功能的时候,我们也不用担心会破坏其他页面。

      现在,我们不妨把这两个场景放在一起进行对比。场景一每一个功能都足够独立,这样每个功能就很容易实现,而且也容易维护。照此来看,要隔离复杂度,那我们要做的就是把一个复杂的系统模块化,并把每个模块之间的依赖降到最低。这样,每当增加新的功能时,也就不会增加整个系统的复杂度了。

        事实上,我在程序职业生涯中,很大一部分时间都是在思考和解决这个问题:如何降低依赖,让整个大型应用的复杂度始终在可控范围内。

        只有这样,在团队内,无论是代码写得比较初级的新手,还是总想尝试新技术新方式的探索者,再或者是代码写得很漂亮的老手。他们都能在各自的功能模块内完成业务功能,而且尽可能少地互相影响,从而始终保证整个架构的可扩展。你可能会说,这个目标听上去有点理想化啊,毕竟模块之间不可能没有互相依赖。我完全理解这样的想法,但是我更想强调的是:只有先制定一个清晰且正确的目标,那么我们之后在实际项目开发中才能尽量去靠近这个目标。那么既然要隔离复杂度,首先要做的就是在物理层面,让源代码能够按功能模块组织在一起,也就是要正确地按领域去组织你的项目文件夹结构

      按领域组织文件夹结构

        如果你以前做过java项目,一定会熟悉分层架构,你经常会见过各种各样的文件夹组织方式。

        前端项目, 由于后端java分层架构的影响,源代码是从技术角度进行划分的。这对于一个小的功能,没有什么问题。但是对于一个大型的应用来说,这种做法就会导致整个应用的不可扩展和维护。我看到很多公司的做法是在components、store、css 等文件夹下,再按照功能进行分类。而这个分类的做法呢,经常是按照技术功能进行进一步划分,比如 table、modals、pages 等。这种做法其实会增加项目结构的复杂度,开发起来也很不方便,主要体现在三个方面。

        1,对于一个功能,我们无法直观地知道它相关的代码散落在哪些文件夹中。

        比如一个页面的详情,可能有列表、下拉框、对话框、抽屉、异步请求逻辑等等,包括组件的状态,它们都在不同的文件夹中。

      2,开发一个功能时,切换源代码会非常不方便。

        比如你在写列表功能时,就需要在组件、样式文件、store等等文件夹之间频繁地来回切换。而且,如果项目很大,那么你就需要展开很长的树结构,才能找到相应的文件,或者借助文件搜索去导航。不过,文件搜索导航的前提是,你还需要对整个功能的逻辑非常了解,知道有哪些文件。

      3,文件之间的交叉引用,强耦合

        比如修改一个详情功能,就需要在相应的组件、store等等文件夹中的对应文件修改代码。而且,你修改完代码都不知道都影响哪些页面,哪些功能,变的项目的复杂度不可控制。

        产生这种开发难度的本质就在于,源代码没有按照业务功能组织在一起,而是从技术角度进行了拆分。所以呢,对于文件夹的组织,我们一定要按领域去组织源代码。一个与领域相关的文件夹,就类似于刚才讲的第一个场景,自身包含了自己需要的所有技术模块,这样无论是理解代码实现,还是开发时切换导航,都会非常方便。

      那我们该如何组织代码呢?

      首先一个 React 应用,一定是由一些技术部件组成的,比如 component、routing、css、store 等,那React组件是由什么组成?答案是 css + component + store + api等。

        假如我们做一个页面叫做shop页面,  我们根据页面UI来划分组件, 比如有一个创建商铺组件, 一个shop详情组件。文件夹结构如下图:

      可以看到,我们把业务模块下的一个名为createShop组件和shopDetail的组件都放到了shop的文件夹下,这样就可以和其它一些业务代码区分开来。一旦我们按照领域组织了项目的功能文件夹,那么,在每一个独立的功能下面,无论怎么组织源代码,都不会有太大的问题,因为都是很小的文件夹。比如上面这张图中,我们把createShop组件用到的css、api、store以及component.jsx文件都放在了createShop的文件夹下,只作用于组件本身,而shop文件夹下面index文件是来组合createShop和shopDetail组件。

        这样,我们copy或delete一个文件夹,就等于copy或delete了一个组件,真正的达到高内聚低耦合,我们只关心组件需要的props,不需要太关心组件内部是怎么实现的,修改这个组件,只作用于组件本身,不会影响其他组件。同时呢,我们也要尽量扁平化地组织所有代码,而不是再去按小的功能去增加嵌套的文件夹。否则,如果你再回头去看代码,或者新加入的成员去看,会增加理解成本。

      处理模块间的依赖

      当我们通过文件夹对业务模块进行了隔离之后,接下来就要考虑该怎样在模块之间进行交互了。这里需要注意的是,尽管各个模块的代码已经处在独立的文件夹之中了,但其实还是可以互相引用的,也就是可以任意依赖。           直观上来说,依赖是在代码中 import 了另外一个模块,但是如果对应到业务,那么它的本质是什么呢?仔细思考下,我们其实可以把依赖从技术层面提升到业务层面,也就是一个业务功能对另外一个业务功能的依赖。

      从业务功能去理解,依赖可以分为两种。

        第一种是硬依赖。如果功能 A 的实现必须基于功能 B,也就是说没有功能 B,功能 A 就是不可运行的,那么我们可以说 A 硬依赖于 B。

      第二种是软依赖。如果功能 B 扩展了功能 A,也就是说,没有功能 B,功能 A 自身也是可以独立工作的,只是缺少了某些能力。

        而我们要达到的目标其实是:删除或者复制一个功能,就像删除或复制一个文件夹那么简单。这才是真正松耦合的系统。

        所以呢,虽然在业务功能上是一个软依赖,但是在代码实现层面,却往往做成了硬依赖。这就导致随着功能的不断增加,整个应用变得越来越复杂,最终降低了整体的开发效率。既然如此,我们就必须想办法,让模块之间的交互不再通过硬依赖。在这里,我推荐介绍一种架构,那就是扩展点机制:在任何可能产生单点复杂度的模块中,通过扩展点的方式,允许其它模块为其增加功能。

      我们该怎么去实现这样一个扩展点引擎呢?可能直觉上挺复杂,但其实并不困难,我们可以利用类似事件的订阅和发布模型去建立这样一个机制。

        好了,相信大家了解了为什么要正确地按领域去组织你的项目文件夹结构这个问题了。接下来我再和大家讨论一下状态管理的问题。

      大家做react项目,编写组件首先要解决的一个问题就是如何设计props和state?

      想弄明白这个问题, 我们就要考虑一下props和state的特性,大家应该都知道,props在组件内部是不可以修改,组件外部可以修改。state是在组件内部可以修改,但是组件外部是不可直接修改。很明显:

      props:相当于定义在组件中对外开放的接口,用来传递状态。

      state:是定义在组件内部的,用来记录内部状态,控制不同时机渲染不同内容。

由此可见,React是用state做内聚,用props来解耦。

    我们已经弄明白上述问题,接下来我们讨论怎么设计state,设计state必须遵守三个原则,

    原则一: state最小化原则

        在实际开发的过程中,很多复杂场景之所以变得复杂,如果抽丝剥茧来看,你会发现它们都有定义多余状态现象的影子,多余状态定义多了,而我们就要花很大的代价来维持状态的一致性,从而使得组件变的臃肿而难以维护。 之所以出现这样的问题,根源就在于它们没有遵循状态最小化原则。

        所以我们在定义一个新的状态之前,都要再三问自己:这个状态是必须的吗?是否能通过计算得到呢?在得到肯定的回答后,我们再去定义新的状态,就能避免大部分多余的状态定义问题了,也就能在简化状态管理的同时,保证状态的一致性。

    原则二: 避免中间状态,确保唯一数据源

        在有的场景下,特别是原始状态数据来自某个外部数据源,而非 state 或者 props 的时候,冗余状态就没那么明显。这时候你就需要准确定位状态的数据源究竟是什么,并且在开发中确保它始终是唯一的数据源,以此避免定义中间状态。

    原则三:避免状态嵌套太深,state扁平化原则

      在有的时候,好多人为了定义状态方便,把所有状态定义到了一个对象里面,并且嵌套很深,可想而知,我们想要改变状态不是很方便,使用状态时,导致我们不停的判空防止程序出错。

怎么设计state,相信大家已经非常明白, 那我们最后聊一下全局状态管理库

相信大家都了解 Redux 和 MobX,  不的不说,大家在用这两个库的时候,写法各异。我列举两个我见过的案例:

案例一: 把所有组件用到的state全部搬到Redux或MobX store里面。

案例二: 把几个组件公用的状态写到Redux或MobX store里面。

那到底怎么用这个全局状态管理库呢?我们先来了解一下Redux 和 MobX到底是解决什么问题?

Redux store 或 MobX store有两个特点:

1、Store 是全局唯一的,一般整个应用程序只有一个 Store。

2、Store 是树状结构,可以更自然地映射到组件树的结构,虽然不是必须的。

        我们通过把状态放在组件之外,就可以让 React 组件成为更加纯粹的表现层,那么很多对于业务数据和状态数据的管理,就都可以在组件之外去完成。同时这也天然提供了状态共享的能力,有两个场景可以典型地体现出这一点。

一、跨组件的状态共享:当某个组件发起一个请求时,将某个 Loading 的数据状态设为 True,另一个全局状态组件则显示 Loading 的状态。

二、同组件多个实例的状态共享:某个页面组件初次加载时,会发送请求拿回了一个数据,切换到另外一个页面后又返回。这时数据已经存在,无需重新加载。设想如果是本地的组件 state,那么组件销毁后重新创建,state 也会被重置,就还需要重新获取数据。

        从上面可以看出, Redux 或 MobX大部分是用来解决跨组件的状态共享同组件多个实例的状态共享。

接下来我们来聊一下引入全局管理状态库的优缺点把:

引入Redux缺点: 

1、组件与store耦合度高, 组件本身失去高内聚低耦合的设计原则。

2、学习成本高,新手很难上手,写法太繁琐。

3、引入redux,也势必会引入一些其他中间件,导致打包过大。

4、高阶组件增加。

5、滥用store,组件销毁时,需要手动清理store, 如若不手动清理,会导致内存泄漏。

引入MobX缺点:

1、组件与store耦合度高, 组件本身失去高内聚低耦合的设计原则。

2、mobx api 本身不符合函数式编程特点,react推崇函数式编程。

3、引入MobX,导致打包过大。

4、内部许多闭包以及tojs的使用,会导致性能问题。

5、滥用store,组件销毁时,需要手动清理store, 如若不手动清理,会导致内存泄漏。

6、对象可变, 不利用追踪状态。

        在raect16之前,context API还在实验阶段,现context趋于稳定,我们可以用context来替代 Redux 或 MobX。

        现在大家在可以尝试想一下全局状态管理库的优点,对比一下,看看项目引入全局状态管理库是否利大于弊。综上所述,我个人建议,不需要引入全局状态管理库。但是我也不反对使用。

时间不早了,今天和大家聊到这里。希望对大家有所帮助,下次有空和大家聊聊怎么建设前端微服务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容