今天在修改其他人的一份 vue 前端代码时,在重用一个组件遇到很多问题。主要问题是这个组件很复杂,在组件里面以及组件的子组件里面,有大量不同组件会依赖 状态管理/路由参数 进行更新。 这个组件和应用整体的情况基本一样,虽然做了很多封装(大部分 重复/公用 的组件都做了封装),但总让人感觉代码非常分散,无法聚合(改一个地方可能涉及多处代码, 引用组件需要修改组件的内部逻辑等)。
本文尝试从该项目来描述前端开发中一些可能比较严重的问题,思考为什么出现这样的问题,自己如何避免这样的问题: 1、高耦合、低内聚; 2、多数据源; 3、其他问题;
我在调用一个组件时,里面有三十几个子组件,十几个地方依赖了状态管理和路由参数和一些后端数据交互,甚至里面还有websocket链接。当我需要重用这个组件时,根本没办法用,只能重构。 当封装组件的时候,应该是在组件的接口(e.g. props)中暴露组件需要的外部数据,而非在组件里面或者组件的子组件里面依赖外部数据导致过高的耦合。
里面有一个页面,页面中有tab的例子,当切换tab的时候需要做一堆事情:
const activeName = ref('firstTab')
// 查询列表
getList().then((res)=> {...})
// 查询当前时间
getCurrentTime((res)=> {...})
// 设置状态管理
setStore({'curr': activeName})
// 显示一个弹窗
if(...) {
dialog(...)
}
其实可能当时开始开发的时候,这么一看是没有问题的,但后面加了一个变量,就变成了
const activeName = ref('first')
const currentType = store.state.cate
// 查询列表
getListApi().then((res) => {
if(currentType)
... // 各种逻辑和序列化
})
// 查询tab的状态
getCurrentTabStatusApi((res) => {
... // 各种逻辑和序列化
})
// 设置状态管理
if(!currentType) {
setStore({'curr': activeName})
} else {
setStore(...)
}
// 显示一个提示弹窗
dialog(() => {
if(...){}else{}
})
后面再加一个变量,这代码就没办法控制了,事实上这里的代码量比我描述要恐怖很多。
在vue开发过程中, MVVM 的设计模式下,如果模块化做得不够细,会让 viewModel 变得非常复杂,变得复杂的同时无法复用或者移植。一般来说在比较复杂的应用中,页面级别的模块只做对各个子组件的调用,流程控制以及页面级别的变量控制(单一职责)。另外,在钩子函数(或其他 控制器 )不应该写具体的代码实现(单一职责),而应该只是调用 具体/抽象 的实现; 例如上面的代码,应该把 Tab 组件抽离出来, 同时在 钩子函数(e.g. onMounted) 只作为 控制器 去调用具体方法而不做具体实现:
const activeName = ref('first')
const currentType = store.state.cate
onMounted(() => {
setUserState()
showDialog()
})
// 设置用户当前状态
function setUserState() {
if(!currentType) {
setStore({'curr': activeName})
} else {
setStore(...)
}
}
// 是否显示弹窗, 显示的弹窗类型
function showDialog() {
dialog(() => {
if(currentType){}else{}
})
}
<template>
<Tab :activeName="activeName" />
</template>
其实上面可以进一步抽象,当组件足够复杂,例如上面的 Dialog 其实是有几种分类的,可以根据依赖倒置的设计,而是通过抽象来调用具体实现,可以将弹窗功能聚合在一起。例如当前有三种弹窗类型
// dialog.component.vue 三种弹窗类型, normal/newUser/coupon
<script setup>
const props = defineProps({
dialogType: {
type: String,
default: 'normal'
},
})
</script>
<template>
<el-dialog>...</<el-dialog>
</template>
举个例子,页面里面的某个组件需要根据页面路由的参数来做一些业务逻辑处理:
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const currentId = computed(() => route.query.id)
</script>
但后面为了方便(不用每个页面都用url参数传递),又将路由参数写入了 pinia 中给其他 页面/组件 读取; 后面的 页面/组件 有时候又将参数封装到响应式对象中:
import { reactive } from 'vue'
import { useStore } from '@/store/modules/group'
const store = useStore()
const data = reactive({
id: store.gInfo.id
})
不管是对自己或者其他协同开发的人来说都很难处理(用哪个数据读?用哪个数据写?几个数据源如何同步?). 所以应该有一个统一数据源出入口。对于这个项目来说,因为使用了 pinia 作为状态管理,并且需求需要在某些场景下用户通过url参数进入,所以可以在业务逻辑中统一使用 pinia 读取状态.在路由管理中用vue-router的 路由守卫[1] 在路由 初始化/切换时 获取需要的路由参数并写入 pinia 的状态管理中.
1、没有正确使用单例模式; 发现项目很多时候其实想要写单例模式,但写的很奇怪(例如在模块上嵌套一层无意义的单例引入导致一些this指向的问题,例如需要的是饿汉模式但却使用的饱汉模式导致每次都要加各种实例的判断)。其实单例模式在前端开发中经常用到,也可以看一下我之前的文章ECMAscript单例模式和模块化[2],这里不再深入了。
2、大量的全局变量; auto-import, i18n, 自定义全局组件等。建议手动引入一下,全局变量非常不友好(不熟悉项目的人找不到依赖,没有提示,没办法点击跳转等)。
3、最简单的书写规范,例如我的在 vue setup 中的书写风格如下(其实怎么个顺序自己定好都行,但总会有些人写着写着就来个声明,写着写着又写个生命周期钩子,还上千行代码,真的没法看...):
<script setup>
// 第一部分是 引入依赖,变量声明
// 第二部分是 watch api/vue生命周期钩子等
// 第三部分是 自定义方法
</script>
<template>
</template>
<style lange='less' scoped>
</style>