vue中拆分组件的实战案例

Vue
455
0
0
2023-06-28
目录
  • 一、组件化诞生的历史
  • 二、为什么业务组件越开发越难维护
  • 人的问题
  • 技术问题
  • 2.1 项目现状
  • 2.2 理想目标
  • 三、举一个实际的例子
  • 3.1 需求背景
  • 3.2 开发之前: 前端设计文档
  • 数据流向图
  • 目录结构
  • 逻辑控制
  • 拆分的原则
  • 3.3 受控组件和非受控组件
  • 3.4 开发进行: 逻辑变量和UI变量
  • 四、持续的优化
  • 五、可能的问题
  • 五、实践是学习前端的捷径
  • 总结

组件化是一种思维的表现,这种技能映射到人的本质是,一个人是否有能力把一个复杂的问题拆解、简单化的能力。

一、组件化诞生的历史

我们在讨论如何拆分组件之前,是有必要简单的了解一下组件化诞生的一个历史。

前端娱乐圈有一个独有的生态:框架。每年出现的框架层出不穷,根本学不完。但是总的来说还是可以分成两个阶段。

第一阶段: 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
  • 为了维护对于数据的溯源

五、实践是学习前端的捷径

前端是一门手艺活,只有实践才能够提高技术. 前端的天花板确实相比其他方向的低,但是也不是我这样的普通人说能够触碰就能触碰到的. 就算很多高端大佬嗤之以鼻的业务代码, 写的时候如果不多思考如何写的简洁,怎么写优雅,写十年和写三年也是没有差别的.

业务才能创造价值, 有了价值才能有我们前端工程师生存的空间. 所以为了提升自己的价值, 提升自己的工资. 平时写业务代码的时候,想想这样写会有什么问题, 如何写才能够更加好. 在这个基础上, 才能看明白那些框架存在的意义. 业务是在轮子之上的,如果对业务的代码都不理解, 又怎么能够真正的写好轮子呢?

所以我们在保障业务按时完成的情况下,应该多尝试,多实践.