最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用Vue3的CompositionAPI来优化代码量

    正文概述 掘金(神奇的程序员)   2021-01-15   458

    前言

    在我的开源项目中有一个组件是用来发送消息和展示消息的,这个组件的逻辑很复杂也是我整个项目的灵魂所在,单文件代码有1100多行。我每次用webstorm编辑这个文件时,电脑cpu温度都会飙升并伴随着卡顿。

    就在前几天我终于忍不住了,意识到了Vue2的optionsAPI的缺陷,决定用Vue3的CompositionAPI来解决这个问题,本文就跟大家分享下我在优化过程中踩到的坑以及我所采用的解决方案,欢迎各位感兴趣的开发者阅读本文。

    问题分析

    我们先来看看组件的整体代码结构,如下图所示:

    使用Vue3的CompositionAPI来优化代码量

    • template部分占用267行
    • script部分占用889行
    • style部分为外部引用占用1行

    罪魁祸首就是script部分,本文要优化的就是这一部分的代码,我们再来细看下script中的代码结构:

    • props部分占用6行
    • data部分占用52行
    • created部分占用8行
    • mounted部分占用98行
    • methods部分占用672行
    • emits部分占用6行
    • computed部分占用8行
    • watch部分占用26行

    现在罪魁祸首是methods部分,那么我们只需要把methods部分的代码拆分出去,单文件代码量就大大减少了。

    优化方案

    经过上述分析后,我们已经知道了问题所在,接下来就跟大家分享下我一开始想到的方案以及最终所采用的方案。

    直接拆分成文件

    一开始我觉得既然methods方法占用的行数太多,那么我在src下创建一个methods文件夹,把每个组件中的methods的方法按照组件名进行划分,创建对应的文件夹,在对应的组件文件夹内部,将methods中的方法拆分成独立的ts文件,最后创建index.ts文件,将其进行统一导出,在组件中使用时按需导入index.ts中暴露出来的模块,如下图所示:

    使用Vue3的CompositionAPI来优化代码量

    • 创建methods文件夹
    • 把每个组件中的methods的方法按照组件名进行划分,创建对应的文件夹,即:message-display
    • 将methods中的方法拆分成独立的ts文件,即:message-display文件夹下的ts文件
    • 创建index.ts文件,即:methods下的index.ts文件

    index.ts代码

    如下所示,我们将拆分的模块方法进行导入,然后统一export出去

    import compressPic from "@/methods/message-display/CompressPic";
    import pasteHandle from "@/methods/message-display/PasteHandle";
    
    export { compressPic, pasteHandle };
    

    在组件中使用

    最后,我们在组件中按需导入即可,如下所示:

    import { compressPic, pasteHandle } from "@/methods/index";
    
    export default defineComponent({
        mounted() {
          compressPic();
          pasteHandle();
        }
    })
    

    运行结果

    当我自信满满的开始跑项目时,发现浏览器的控制台报错了,提示我this未定义,突然间我意识到将代码拆分成文件后,this是指向那个文件的,并没有指向当前组件实例,当然可以将this作为参数传进去,但我觉得这样并不妥,用到一个方法就传一个this进去,会产生很多冗余代码,因此这个方案被我pass了。

    使用mixins

    前一个方案因为this的问题以失败告终,在Vue2.x的时候官方提供了mixins来解决this问题,我们使用mixin来定义我们的函数,最后使用mixins进行混入,这样就可以在任意地方使用了。

    由于mixins是全局混入的,一旦有重名的mixin原来的就会被覆盖,所以这个方案也不合适,pass。

    使用Vue3的CompositionAPI来优化代码量

    使用CompositionAPI

    上述两个方案都不合适,那 么CompositionAPI就刚好弥补上述方案的短处,成功的实现了我们想要实现的需求。

    我们先来看看什么是CompositionAPI,正如文档所述,我们可以将原先optionsAPI中定义的函数以及这个函数需要用到的data变量,全部归类到一起,放到setup函数里,功能开发完成后,将组件需要的函数和data在setup进行return。

    setup函数在创建组件之前执行,因此它是没有this的,这个函数可以接收2个参数: props和context,他们的类型定义如下:

    interface Data {
      [key: string]: unknown
    }
    
    interface SetupContext {
      attrs: Data
      slots: Slots
      emit: (event: string, ...args: unknown[]) => void
    }
    function setup(props: Data, context: SetupContext): Data
    

    我的组件需要拿到父组件传过来的props中的值,需要通过emit来向父组件传递数据,props和context这两个参数正好解决了我这个问题。

    setup又是个函数,也就意味着我们可以将所有的函数拆分成独立的ts文件,然后在组件中导入,在setup中将其return给组件即可,这样就很完美的实现了一开始我们一开始所说的的拆分。

    实现思路

    接下来的内容会涉及到响应性API,如果对响应式API不了解的开发者请先移步官方文档。

    我们分析出方案后,接下来我们就来看看具体的实现路:

    • 在组件的导出对象中添加setup属性,传入props和context

    • 在src下创建module文件夹,将拆分出来的功能代码按组件进行划分

    • 将每一个组件中的函数进一步按功能进行细分,此处我分了四个文件夹出来

      • common-methods 公共方法,存放不需要依赖组件实例的方法
      • components-methods 组件方法,存放当前组件模版需要使用的方法
      • main-entrance 主入口,存放setup中使用的函数
      • split-method 拆分出来的方法,存放需要依赖组件实例的方法,setup中函数拆分出来的文件也放在此处
    • 在主入口文件夹中创建InitData.ts文件,该文件用于保存、共享当前组件需要用到的响应式data变量

    • 所有函数拆分完成后,我们在组件中将其导入,在setup中进行return即可

    实现过程

    接下来我们将上述思路进行实现。

    添加setup选项

    我们在vue组件的导出部分,在其对象内部添加setup选项,如下所示:

    <template>
      <!---其他内容省略-->
    </template>
    <script lang="ts">
    export default defineComponent({
      name: "message-display",
      props: {
        listId: String, // 消息id
        messageStatus: Number, // 消息类型
        buddyId: String, // 好友id
        buddyName: String, // 好友昵称
        serverTime: String // 服务器时间
      },
      setup(props, context) {
        // 在此处即可写响应性API提供的方法,注意⚠️此处不能用this
      }
    }
    </script>
    

    创建module模块

    我们在src下创建module文件夹,用于存放我们拆分出来的功能代码文件。

    如下所示,为我创建好的目录,我的划分依据是将相同类别的文件放到一起,每个文件夹的所代表的含义已在实现思路进行说明,此处不作过多解释。

    使用Vue3的CompositionAPI来优化代码量

    创建InitData.ts文件

    我们将组件中用到的响应式数据,统一在这里进行定义,然后在setup中进行return,该文件的部分代码定义如下,完整代码请移步:InitData.ts

    import {
      reactive,
      Ref,
      ref,
      getCurrentInstance,
      ComponentInternalInstance
    } from "vue";
    import {
      emojiObj,
      messageDisplayDataType,
      msgListType,
      toolbarObj
    } from "@/type/ComponentDataType";
    import { Store, useStore } from "vuex";
    
    // DOM操作,必须return否则不会生效
    const messagesContainer = ref<HTMLDivElement | null>(null);
    const msgInputContainer = ref<HTMLDivElement | null>(null);
    const selectImg = ref<HTMLImageElement | null>(null);
    // 响应式Data变量
    const messageContent = ref<string>("");
    const emoticonShowStatus = ref<string>("none");
    const senderMessageList = reactive([]);
    const isBottomOut = ref<boolean>(true);
    let listId = ref<string>("");
    let messageStatus = ref<number>(0);
    let buddyId = ref<string>("");
    let buddyName = ref<string>("");
    let serverTime = ref<string>("");
    let emit: (event: string, ...args: any[]) => void = () => {
      return 0;
    };
    // store与当前实例
    let $store = useStore();
    let currentInstance = getCurrentInstance();
    
    export default function initData(): messageDisplayDataType {
      // 定义set方法,将props中的数据写入当前实例
      const setData = (
        listIdParam: Ref<string>,
        messageStatusParam: Ref<number>,
        buddyIdParam: Ref<string>,
        buddyNameParam: Ref<string>,
        serverTimeParam: Ref<string>,
        emitParam: (event: string, ...args: any[]) => void
      ) => {
        listId = listIdParam;
        messageStatus = messageStatusParam;
        buddyId = buddyIdParam;
        buddyName = buddyNameParam;
        serverTime = serverTimeParam;
        emit = emitParam;
      };
      const setProperty = (
        storeParam: Store<any>,
        instanceParam: ComponentInternalInstance | null
      ) => {
        $store = storeParam;
        currentInstance = instanceParam;
      };
      
      // 返回组件需要的Data
      return {
        messagesContainer,
        msgInputContainer,
        selectImg,
        $store,
        emoticonShowStatus,
        currentInstance,
        // .... 其他部分省略....
        emit
      }
    }
    

    在组件中使用

    定义完相应死变量后,我们就可以在组件中导入使用了,部分代码如下所示,完整代码请移步:message-display.vue

    import initData from "@/module/message-display/main-entrance/InitData";
    
    export default defineComponent({
       setup(props, context) {
        // 初始化组件需要的data数据
        const {
          createDisSrc,
          resourceObj,
          messageContent,
          emoticonShowStatus,
          emojiList,
          toolbarList,
          senderMessageList,
          isBottomOut,
          audioCtx,
          arrFrequency,
          pageStart,
          pageEnd,
          pageNo,
          pageSize,
          sessionMessageData,
          msgListPanelHeight,
          isLoading,
          isLastPage,
          msgTotals,
          isFirstLoading,
          messagesContainer,
          msgInputContainer,
          selectImg
        } = initData();
         
        // 返回组件需要用到的方法
        return {
          createDisSrc,
          resourceObj,
          messageContent,
          emoticonShowStatus,
          emojiList,
          toolbarList,
          senderMessageList,
          isBottomOut,
          audioCtx,
          arrFrequency,
          pageStart,
          pageEnd,
          pageNo,
          pageSize,
          sessionMessageData,
          msgListPanelHeight,
          isLoading,
          isLastPage,
          msgTotals,
          isFirstLoading,
          messagesContainer,
          msgInputContainer,
          selectImg
        };
       }
    })
    

    我们定义后响应式变量后,就可以在拆分出来的文件中导入initData函数,访问里面存储的变量了。

    在文件中访问initData

    我将页面内所有的事件监听也拆分成了文件,放在了EventMonitoring.ts中,在事件监听的处理函数是需要访问initData里存储的变量的,接下来我们就来看下如何访问,部分代码如下所示,完整代码请移步EventMonitoring.ts)

    import {
      computed,
      Ref,
      ComputedRef,
      watch,
      getCurrentInstance,
      toRefs
    } from "vue";
    import { useStore } from "vuex";
    import initData from "@/module/message-display/main-entrance/InitData";
    import { SetupContext } from "@vue/runtime-core";
    import _ from "lodash";
    
    
    export default function eventMonitoring(
      props: messageDisplayPropsType,
      context: SetupContext<any>
    ): {
      userID: ComputedRef<string>;
      onlineUsers: ComputedRef<number>;
    } | void {
      const $store = useStore();
      const currentInstance = getCurrentInstance();
      // 获取传递的参数
      const data = initData();
      // 将props改为响应式
      const prop = toRefs(props);
      // 获取data中的数据
      const senderMessageList = data.senderMessageList;
      const sessionMessageData = data.sessionMessageData;
      const pageStart = data.pageStart;
      const pageEnd = data.pageEnd;
      const pageNo = data.pageNo;
      const isLastPage = data.isLastPage;
      const msgTotals = data.msgTotals;
      const msgListPanelHeight = data.msgListPanelHeight;
      const isLoading = data.isLoading;
      const isFirstLoading = data.isFirstLoading;
      const listId = data.listId;
      const messageStatus = data.messageStatus;
      const buddyId = data.buddyId;
      const buddyName = data.buddyName;
      const serverTime = data.serverTime;
      const messagesContainer = data.messagesContainer as Ref<HTMLDivElement>;
      
      // 监听listID改变
      watch(prop.listId, (newMsgId: string) => {
        listId.value = newMsgId;
        messageStatus.value = prop.messageStatus.value;
        buddyId.value = prop.buddyId.value;
        buddyName.value = prop.buddyName.value;
        serverTime.value = prop.serverTime.value;
        // 消息id发生改变,清空消息列表数据
        senderMessageList.length = 0;
        // 初始化分页数据
        sessionMessageData.length = 0;
        pageStart.value = 0;
        pageEnd.value = 0;
        pageNo.value = 1;
        isLastPage.value = false;
        msgTotals.value = 0;
        msgListPanelHeight.value = 0;
        isLoading.value = false;
        isFirstLoading.value = true;
      });
    }
    

    至此,有关compositionAPI的基本使用就跟大家讲解完了,下面将跟大家分享下我在实现过程中所踩的坑,以及我的解决方案。

    踩坑分享

    今天是周四,我周一开始决定使用CompositionAPI来重构我这个组件的,一直搞到昨天晚上才重构完成,前前后后踩了很多坑,正所谓踩坑越多你越强,这句话还是很有道理的?。

    接下来就跟大家分享下我踩到的一些坑以及我的解决方案。

    dom操作

    我的组件需要对dom进行操作,在optionsAPI中可以使用this.$refs.xxx来访问组件dom,在setup中是没有this的,翻了下官方文档后,发现需要通过ref来定义,如下所示:

    <template>
    <div ref="msgInputContainer"></div>
    <ul v-for="(item, i) in list" :ref="el => { ulContainer[i] = el }"></ul>
    </template>
    
    <script lang="ts">
      import { ref, reactive, onBeforeUpdate } from "vue";
      setup(){
        export default defineComponent({
        // DOM操作,必须return否则不会生效
        // 获取单一dom
        const messagesContainer = ref<HTMLDivElement | null>(null);
        // 获取列表dom
        const ulContainer = ref<HTMLUListElement>([]);
        const list = reactive([1, 2, 3]);
        // 列表dom在组件更新前必须初始化
        onBeforeUpdate(() => {
           ulContainer.value = [];
        });
        return {
          messagesContainer,
          list,
          ulContainer
        }
      })
      }
    </script>
    
    

    访问vuex

    在setup中访问vuex需要通过useStore()来访问,代码如下所示:

    import { useStore } from "vuex";
    
    const $store = useStore();
    console.log($store.state.token);
    

    访问当前实例

    在组件中需要访问挂载在globalProperties上的东西,在setup中就需要通过getCurrentInstance()来访问了,代码如下所示:

    import { getCurrentInstance } from "vue";
    
    const currentInstance = getCurrentInstance();
    currentInstance?.appContext.config.globalProperties.$socket.sendObj({
      code: 200,
      token: $store.state.token,
      userID: $store.state.userID,
      msg: $store.state.userID + "上线"
    });
    

    无法访问$options

    我重构的websocket插件是将监听消息接收方法放在options上的,需要通过this.$options.xxx来访问,文档翻了一圈没找到有关在setup中使用的内容,那看来是不能访问了,那么我只能选择妥协,把插件挂载在options上的方法放到globalProperties上,这样问题就解决了。

    内置方法只能在setup中访问

    如上所述,我们使用到了getCurrentInstanceuseStore,这两个内置方法还有initData中定义的那些响应式数据,只有在setup中使用时才能拿到数据,否则就是null。

    我的文件是拆分出去的,有些函数是运行在某个拆分出来的文件中的,不可能都在setup中执行一遍的,响应式变量也不可能全当作参数进行传递的,为了解决这个问题,我有试过使用provide注入然后通过inject访问,结果运行后发现不好使,控制台报黄色警告说provide和inject只能运行在setup中,我直接裂开,当时发了一条沸点求助了下,到了晚上也没得到解决方案?。

    经过一番求助后,我的好友@前端印象给我提供了一个思路,成功的解决了这个问题,也就是我上面initData的做法,将响应式变量定义在导出函数的外面,这样我们在拆分出来的文件中导入initData方法时,里面的变量都是指向同一个地址,可以直接访问存储在里面的变量且不会将其进行初始化。

    至于getCurrentInstanceuseStore访问出现null的情景,还有props、emit的使用问题,我们可以在initData的导出函数内部定义set方法,在setup里的方法中获取到实例后,通过set方法将其设置仅我们定义的变量中。

    至此,问题就完美解决了,最后跟大家看下优化后的组件代码,393行?

    使用Vue3的CompositionAPI来优化代码量

    项目地址

    项目地址:chat-system-github

    在线体验地址:chat-system

    写在最后

    • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注?
    • 本文首发于掘金,未经许可禁止转载?

    下载网 » 使用Vue3的CompositionAPI来优化代码量

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元