一个底层框架工人的vue3 composition函数式组件使用感受

1,359 阅读6分钟

vue3除去一堆零敲碎打的优化更新之后最让我觉得眼前一亮的就是composition组合式组件以及其对应的函数式写法了。

首先,我们先看下composition组件的用法。其实如果从光从写法上来看和option配置型写法区别不大只不过是换成了函数式,且需要在新的hook setup() 中注册返回一下

具体写法

export default {
  setup() {
    // 相当于data,需要用ref生成reactive对象,对其value值赋值触发依赖收集访问
    const count = ref(0)
    // 相当于computed计算属性
    const plusOne = computed(() => count.value + 1)
    // 相当于method
    const increment = () => { count.value++ }
    // 相当于watch监听
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // 相当于mounted生命周期
    onMounted(() => {
      console.log(`mounted`)
    })
    // 将需要注册到页面中的选项返回
    return {
      count,
      plusOne,
      increment
    }
  }
}

写一个composition组件对于熟练的框架工来说并不难,但还是得稍微深入的了解一下新功能才能用得安心。

首先了解一个新的功能首先要看它为什么出现以及它的出现对现在的开发生态有哪些改变。

在vue2中我们进行开发叫做选项式开发 ,同一个功能逻辑可能需要 在data选项里写写,在methods里动动,生命周期再写两句,开发的时候可能不会觉得有什么。带着耳机听着歌,脸滚键盘业务代码随手写。

但是!码砖一时爽,维护火葬场。特别你要是对业务不熟悉接手别人的代码。真是及其酸爽,记得刚到新公司那会接手上一个同事的代码。一个功能块,把组件的option零零碎碎写在七八本文件里来回引用。一个小功能,半个小时硬是没找全代码块。

那个项目给我留下了巨大的心理阴影,自那以后。我一直都想着怎样能够面向功能块开发,将一个独立完整的业务功能封装成独立的代码块,这样既方便维护。同时封装相同的逻辑进行封装引用 ,也极大的降低了工作量,减少复制黏贴,以及复制黏贴过程中产生的错误。

不光是我,尤雨溪也和我有着同样的困惑,

The separation of options obscures the underlying logical concerns. In addition, when working on a single logical concern, we have to constantly "jump" around option blocks for the relevant code.

就像下图一样,在后期维护大型vue项目的时候,往往改动代码只需要3分钟,找到要改的代码往往需要30分钟。

在这里插入图片描述

所以我们的目的是要变成这样,让颜色相同的功能代码放在一起。这样既方便维护又能逻辑抽离 在这里插入图片描述 在composition之前,实现代码模块化抽离插入,一般都是用mixin。但是在我大量的业务码砖中发现mixin虽好,但仅从业务开发体验来说也有很让人操蛋的地方。

1 重名的问题:这个用过mixin抽业务的应该都深有体会,对于我这种取名困难症患者可以说十分不友好了。

2 隐形依赖的问题:这个人问题在项目不大的时候其实没啥感觉,一旦项目大起来一个页面几十个mixin模块混入,然后各个mixin之前还有data参数和method相互调用。但我们知道mixin是平级引用在同一个页面内的。也就是谁用了谁的参数,根本不需要显示引用。这样其实就有很大的隐患在里面。导致你东西写完了,后期是不太敢动的。搞不好哪天你的同事改了一个data属性可能就会导致一片mixin模块都崩盘。

既然mixin能用但确实有缺点,那看composition与其相比能做到哪些改善。

首先在使用composition时不仅可以是mixin能做的他都能做到。而且还因为他是显示引用就不会有mixin带来的重名和隐形依赖的烦恼。还有就是mixin再怎么抽离归根到底还是option配置型编程,随着业务的铺展,同样会面对抽太碎了文件多语义不清,块太大又过于臃肿。

但是composition他是函数式编程,其带来的好处就是:

1 首先同样的业务不需要再在data和method中饱受分离之苦,业务块可以写在一起块,然后分离出去。

2 其次由于是函数传参和返参方便多了。比如:

我手上很多业务都是上面一个表单下面一个表格的经典管理页面。所以在业务场景差不多的情况下,我一般会把这个业务场景抽出来,但每个页面表格获取数据的接口都不一样,如果用mixin的话,就得在每个页面组件的data内再定义一个属性。同理如果业务臃肿起来,随着混入的mixin模块和请求的接口越来越多,那么data内定义的属性就会越来越多。而且其他页面引用改模块时,页面内data属性名还得保持一致,这样就僵硬死板。

但使用composition函数组件抽出去,我们只需要将请求接口名当成参数传入组件即可,不再需要再在页面组件内定义一个data属性

获取页面数据组件


import { ref, onMounted } from 'vue';
import api from '@/config/api/jsonConfig.js'
import commonData from '@/config/general/commonData.js'
/* 
Fcn:请求表格数据接口名
autoRequest:是否需要挂载完成后自动加载数据
*/
export default (Fcn,autoRequest) => {
    // 表格绑定数据
    const tableData = ref([])
    // 表格页码数据
    const paginationOpt = ref({
        pageNumber: 1,
        pageSize: 20,
        totalRecords: 0
    })
    // 获取表格数据
    const getTableData = async (params) => {
        let d = await api[Fcn]({ params: { ...params, organizationUID: commonData.currentOrg.organizationUID } })
        if (!d) return

        tableData.value =d.data.pagination? d.data.records:d.data
        paginationOpt.value = d.data.pagination
    }
    // 翻页
    const handleSizeChange = (v) => {
        paginationOpt.value.pageSize = v
        getTableData(paginationOpt.value)
    }
    const handleCurrentChange = (v) => {
        paginationOpt.value.pageNumber = v
        getTableData(paginationOpt.value)
    }
    // 挂载完成请求数据
    onMounted(() => {
        if(autoRequest) getTableData()
    })
    return { tableData, paginationOpt, getTableData, handleSizeChange, handleCurrentChange }
}



页面组件:只需要将方法名传入即可


export default defineComponent({
  setup() {
    return {
      ...useTable("examSourcegetList",true),
    };
  },
});

3 抽离层级更好控制,在之前使用mixin中我们在页面中混入一个模块,那就代表这接受这个模块所有的配置。那如果A,B页面需要模块的所有业务选项,C页面却只需要一部分数据和方法。就很不好处理。 但是在composition组件中因为组件需要在setup()中return才会注册到页面组件中,所以我们可以放心大胆的按业务功能块进行抽离,在不需要所有业务块的C页面中我们只return其需要的属性方法即可。

就比如我把一个功能块所有的select下拉填充的绑定数据和获取数据的方法都写在一个组件内方便管理。在不同的页面我只需要返回其业务需要的下拉填充即可。这在mixin内是没办法实现的

// 下拉填充
import { ref } from 'vue'
import api from '@/config/api/jsonConfig.js'
export default () => {
    // RMCO机构
    let RMCOorgList = ref([])
    // 签约服务中心
    let ServiceCenterList = ref([])
    // 科室列表下拉框
    let DepartmentList = ref([])
    // 检查类型
    let ServiceSectList = ref([])
    // 检查状态
    let ExamStatusList = ref([])
    // 就诊类别
    let PatientClassList = ref([])
    // 上传规则
    let UploadRuleList = ref([])
    // 数据源
    let ExamSourceList = ref([])
    // 影像配置
    let PACSSourceList = ref([])
    
    const getRMCOrganizationList = async (params) => {
         ...
    }
    const getSignedServiceCenterList = async (params) => {
        ...
    }
    const getDepartmentList = async (params) => {
        ...
    }
    const getServiceSectList = async (params) => {
        ...
    }
    const getExamStatusList = async (params) => {
      ...
    }
  ...
    return {
        RMCOorgList,
        ServiceCenterList,
        DepartmentList,
        ServiceSectList,
        ExamStatusList,
        PatientClassList,
        gainTypeList,
        dbTypeList,
        ....
    }
}

页面组件内只需要引入返回对应的数据选项即可

import userDropOptions from "@/components/composition/dropOptions";

setup(props,{emit}) {
    // 获取下拉相关
    const { depGainTypeList, dbTypeList } = userDropOptions();
    }
     return {
       depGainTypeList,
       dbTypeList,
    };