我们先来看看组件的整体代码结构:
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中暴露出来的模块。
创建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。
使用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文件夹,用于存放我们拆分出来的功能代码文件。
创建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 } }
细心的开发者可能已经发现,我把响应式变量定义在导出的函数外面了,之所以这么做是因为setup的一些特殊原因,在下面的踩坑章节我将会详解我为什么要这样做。
在组件中使用
定义完相应死变量后,我们就可以在组件中导入使用了,部分代码如下所示,完整代码请移步: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; }); }
正如代码中那样,在文件中使用时,拿出initdata中对应的变量,需要修改其值时,只需要修改他的value即可。
至此,有关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中访问
如上所述,我们使用到了getcurrentinstance和usestore,这两个内置方法还有initdata中定义的那些响应式数据,只有在setup中使用时才能拿到数据,否则就是null。
我的文件是拆分出去的,有些函数是运行在某个拆分出来的文件中的,不可能都在setup中执行一遍的,响应式变量也不可能全当作参数进行传递的,为了解决这个问题,我有试过使用provide注入然后通过inject访问,结果运行后发现不好使,控制台报黄色警告说provide和inject只能运行在setup中,我直接裂开,当时发了一条沸点求助了下,到了晚上也没得到解决方案。
经过一番求助后,我的好友@前端印象给我提供了一个思路,成功的解决了这个问题,也就是我上面initdata的做法,将响应式变量定义在导出函数的外面,这样我们在拆分出来的文件中导入initdata方法时,里面的变量都是指向同一个地址,可以直接访问存储在里面的变量且不会将其进行初始化。
至于getcurrentinstance和usestore访问出现null的情景,还有props、emit的使用问题,我们可以在initdata的导出函数内部定义set方法,在setup里的方法中获取到实例后,通过set方法将其设置进我们定义的变量中。
以上就是vue3如何用compositionapi优化代码量的详细内容。