vue中拆分组件的实战案例
我是小橘子哦 lv-4 人气:0组件化是一种思维的表现,这种技能映射到人的本质是,一个人是否有能力把一个复杂的问题拆解、简单化的能力。
一、组件化诞生的历史
我们在讨论如何拆分组件之前,是有必要简单的了解一下组件化诞生的一个历史。
前端娱乐圈有一个独有的生态:框架。每年出现的框架层出不穷,根本学不完。但是总的来说还是可以分成两个阶段。
第一阶段: JQ和PrototypeJS。 该阶段解决了浏览器的兼容性问题以及API的遍历程度
第二阶段: Vue、React、Angular。解决了组件化、解耦、复用等问题
在大陆,主要讨论的是Vue和React。 有些人说Vue是framework,而React是library,前者有更多的约束和更加齐全的工具链。而后者更加的自由。但是真的要投入生产的话,依旧需求认为的给React添加很多的约束,而且Vue也是支持jsx的,所以我一直不太赞同React更加自由这样的说话。
在我看来,它们在实际生产开发过程中,在那一堆工具链中,只是API的不同而已。
它们都为前端提供了很好的组件化。而且近一年来两者都不约而同朝着函数式跟进。它们带来的各种hook,给我们带来了不一致的组件化的写法。
二、为什么业务组件越开发越难维护
人的问题
当然是人的问题. 或许产品的问题,或许整个工作流程的问题,或许上面的问题. 这些我们暂且不提,我作为开发, 首先是要管好自己的代码组织.
再次我们先排除其他外界的因素,比如产品经常改需求. 仅从编码阶段来说.
以我们团队为例,我们团队内部员工2个,8个外包,外包兄弟们的招聘标准是远低于内部的。团队人员每个人的编码能力差距还是很大的。项目都是长期维护的,一个业务模块就会有很多人维护,在上面不断的填尿加屎。
在这里并不是说外包人员的编码能力差,我们组就有一个外包的兄弟编码能力、解决问题的能力相当厉害的,比很多内部的都好很多。这里只是从平局值上面来说。
团队成员的水平参差不齐, 顾及到团队协作, 我们在拆分组件的时候需要更加的简单和清晰.
技术问题
业务逻辑和交互逻辑的纠缠不清
2.1 项目现状
以该图为例, A B C 分别是父子孙组件. 当我们要控制其中一个组件的状态的是, 可以通过很多方式来进行控制. 这些方式的来源有可能是全局变量、vuex、时间总线、来自自己父组件或子组件的改变等等.
可以看出, 改变它组件内部状态的来源非常的多, 维护或者修改的时候,需要翻阅的文件目录和范围就很广. 自然就很难维护.
举一个mixins的例子:
假设它混入了这么多功能。
export default { mixins: [ a, b, c, d, e, f, g], mounted() { console.log(this.whoAreYou) } }
这个this.whoAreYou
你能够知道来源于哪一个么? 而如果改成hook
的写法来引入某个JS中的变量:
const { IamI } = myHome() const { IamI as me } = myHome()
这就很简洁干净。在你维护代码的时候,可以很好的进行溯源。 而上面的一切,导致难以维护的原因总结来说有两个:
- 混用业务变量和UI变量
- 不区分受控组件和非受控组件
下面我会实际例子分别介绍这个两个概念。而基于hooks
的复用才是我们现在解决组件化复用的更好的选择。
2.2 理想目标
基于hook
的理想模型
依旧是A B C 三个组件.但是A B C三个组件外边飘的那些箭头不存在了. 所有能够控制它们的内部状态的方式都集中 在了controllers
上面.
其中controllers
部分的组织形式和vue的composition api
宣传图表现一致。
将相似的功能以及用到的变量都封装在一个函数当中。这一切也更加好的迎合了
实际代码如下:
<template> <div> <B setC={setC} /> <div> </template> <script setup> import B from 'B.js' import cController from 'cController.js' const { setC } = cController(props) </script> // cController.js export default c(props) { const c = ref('') const setC() { c.value = 'I an cController' } return { c, setC, } }
cController.js
就是controllers
中的一个void
. 引入到A组件当中,然后将里面的方法通过props传给B组件.
<template> <div> <C setC={setC} /> <div> </template> <script setup> import C from 'C.js' props: { setC: { type: Number, } } </script>
也就是说,控制C组件内部状态的是通过引入到A组件中的controller
来进行通过,中间的B组件不做任何的处理,仅仅作为一个中转站. 操作起来和理论都很简单。但是想要更好的拆分的话,还需要了解三个概念:
- 业务变脸和UI变量
- 受控组件和非受控组件
- 控制反转ioc
下面我通过一个实际的业务场景来描述。
三、举一个实际的例子
3.1 需求背景
简单的截两张图. 需求大致如下:
- 功能就是典型的笔记软件的功能,右边可以放各种类型的文件,点击就可以在右边渲染出对应的内容.
- 目录树有两个彩蛋,会根据当前文件类型出现不同的操作
- 目录树下面有一个固定的收藏夹,目录树可以在这其中滚动
3.2 开发之前: 前端设计文档
数据流向图
功能还是很清楚的,但是功能其实很多. 我认为我们团队在开发之前是必须要有的. 作为一个前端, 可以没有流程图,但是一定要下面这样的图. 我在别的地方没有见过这样的图,所以自己给这样的图做了一个定义,叫数据流向图.
关于完整的工作流程,之后再写一篇文章进行描述
它是有两部分构成:
- 组件的模块
- 组件之间的控制关系
第一点, 还是比较清楚,就是这个需求可以拆成哪几个模块.
第二点, tree组件和content组件是同级组件, tree可以控制content组件内的状态, content组件也可以改变tree内的状态. 再深入一点说,就是tree点击不同的文件类型, content组件部分就会渲染不同的模块; 而当在content组件内对当前阅读的文件进行删除操作的时候,tree作为目录树自然是要刷新最新的目录信息的.
目录结构
通过上面的结构图,可以得到下面这样目录结构.
逻辑控制
数据流向图中的各个组件都放在根目录下index.vue
中挂载. 如下入
控制目录树的相关逻辑都放在listTreeController
控制器里边, 和右边内容content相关逻辑都放入到renderContentController
的方法当中.
随后将controller中公共方法都传进到组件当中. doc-aside
是包括search
和tree
已经other
三个模块的中转组件. 不在这个组件中做任何的逻辑处理. 如下图:
举一个例子, 控制按钮的权限. [背景]
- 所有功能点都受控挂载在vuex的store上面的一个变量, 没有权限的话,就直接通过
v-if
来隐藏对应的入口
[之前实现]
- 直接找到对应的按钮在
v-if
上,通过root.docAuth('createDoc')
来判断
[修改之后]
- 创建来一个
authoControllers.js
在index.vue
引入, 需要用的地方是应用的是
[具体实现]
export default function authController({ root }) { const menuAuth = { [MENTY_TYPE.rename]: root.docAuth('rename'), [MENTY_TYPE.delete]: root.docAuth('delete') // .... } }
虽然在Index.vue中引入,不管是通过props,还是通过依赖注入来给子组件来使用,都不重要.重要的是,它统一管理, 并在index.vue中引入是唯一一个入口. 当我们维护的时候, 只需要通过子组件一路找到对应的controller
就可以找到对应的逻辑了.
拆分的原则
- 对于组件的拆分一开始不需要太细
- 拆分好受控组件和非受控组件
3.3 受控组件和非受控组件
我们使用的任何UI框架都是受控组件, 受控组件的概念就是它里面的状态都是受调用它的组件来控制的. 非受控组件反之.
3.4 开发进行: 逻辑变量和UI变量
UI变量其实很好理解. 像element-ui的组件中所需要的属性就是UI变量. 但是对于我们实际业务当中, 会对这些进行一定扩展.
举一个例子, 在上面的目录中dialog组件
的显示或隐藏,是通过model-value / v-model
来进行控制的, true
就显示, false
就隐藏起来.
隐藏和显示的渐入渐出效果是elementUI框架内置的.
平时工作中很多人是这样传的:
<el-dialog :v-model="data.id === XXXX"> // code </el-dialog> props = { data: { type: Object } }
通过通过接口拿到的,或者自己组件的数据传进来之后,再进行对v-model
的控制. data.id
这样的变量就是业务变量, 通过业务变量来直接控制UI的组件的显示和隐藏,就是业务变量和UI变量的混用. 或者说**业务逻辑和交互逻辑的混用. **
混用之后的后果,就是我们进行维护的时候, 需要查看的变量或者说字段就成倍的增加, 交互变量和业务变量交织在一起. 这部分的代码同时承载了业务逻辑和交互逻辑.
DDD领域模型也是可以解决这个问题, 之后我会再开篇幅聊一聊.
所以我们就需要将业务逻辑和交互逻辑给拆开. 如下:
<template> <el-dialog :v-model="isShow"> <template slot="header"> {{ dialogTitle }} </template> <template slot="content"> // type === 创建表单 // type === 移动文件夹目录 </template> </el-dialog> </temaplte> props = { isShow: { type : Boolean, desc: '是否显示弹窗' }, type: { type: String, desc: '弹窗的类型' } }
其中ishow
和type
就可以视为UI变量, 它们不关心外界是通过了什么判断, 只关系传进来的是true还是false.
四、持续的优化
不管一开始代码是如何规划的,如何组织的.最重要的还是要持续的去维护. 屎山到了之后, 前面的维护者没有一个人是无辜的. 但是也不需要过早的去维护.什么时候到了维护重构的时机呢?
- 当碰到这里用的代码别的地方也用到的时候
- 这个变量出现在好几个地方,被好几个地方都set的时候, 而自己搞不懂它们set的顺序的时候
- 函数复杂到自己看了半天都看不明白的时候
五、可能的问题
问题一: 中转的组件没有挂载任何逻辑,为什么还存在?
- 为了之后可能的拆分
- 让结构更加的清晰
问题二: 中转的组件要挂载这么多办法, 或许太难看?
- 实在是太多可以使用vue的
$attr
和$listeners
- 为了维护对于数据的溯源
五、实践是学习前端的捷径
前端是一门手艺活,只有实践才能够提高技术. 前端的天花板确实相比其他方向的低,但是也不是我这样的普通人说能够触碰就能触碰到的. 就算很多高端大佬嗤之以鼻的业务代码, 写的时候如果不多思考如何写的简洁,怎么写优雅,写十年和写三年也是没有差别的.
业务才能创造价值, 有了价值才能有我们前端工程师生存的空间. 所以为了提升自己的价值, 提升自己的工资. 平时写业务代码的时候,想想这样写会有什么问题, 如何写才能够更加好. 在这个基础上, 才能看明白那些框架存在的意义. 业务是在轮子之上的,如果对业务的代码都不理解, 又怎么能够真正的写好轮子呢?
所以我们在保障业务按时完成的情况下,应该多尝试,多实践.
总结
加载全部内容