在个人开源项目中有一个组件是用来发送消息和展现消息的,这个组件的逻辑很复杂也是我整个项目的灵魂所在,单文件代码有1100多行。我每次用webstorm编辑这个文件时,电脑cpu温度都会飙升并伴随着卡顿。html
就在前几天我终于忍不住了,意识到了Vue2的optionsAPI的缺陷,决定用Vue3的CompositionAPI来解决这个问题,本文就跟你们分享下我在优化过程当中踩到的坑以及我所采用的解决方案,欢迎各位感兴趣的开发者阅读本文。前端
咱们先来看看组件的总体代码结构,以下图所示:vue
罪魁祸首就是script部分,本文要优化的就是这一部分的代码,咱们再来细看下script中的代码结构:react
如今罪魁祸首是methods部分,那么咱们只须要把methods部分的代码拆分出去,单文件代码量就大大减小了。git
通过上述分析后,咱们已经知道了问题所在,接下来就跟你们分享下我一开始想到的方案以及最终所采用的方案。github
一开始我以为既然methods方法占用的行数太多,那么我在src下建立一个methods文件夹,把每一个组件中的methods的方法按照组件名进行划分,建立对应的文件夹,在对应的组件文件夹内部,将methods中的方法拆分红独立的ts文件,最后建立index.ts文件,将其进行统一导出,在组件中使用时按需导入index.ts中暴露出来的模块,以下图所示:web
以下所示,咱们将拆分的模块方法进行导入,而后统一export出去vuex
import compressPic from "@/methods/message-display/CompressPic";
import pasteHandle from "@/methods/message-display/PasteHandle";
export { compressPic, pasteHandle };
复制代码
最后,咱们在组件中按需导入便可,以下所示:typescript
import { compressPic, pasteHandle } from "@/methods/index";
export default defineComponent({
mounted() {
compressPic();
pasteHandle();
}
})
复制代码
当我自信满满的开始跑项目时,发现浏览器的控制台报错了,提示我this未定义,忽然间我意识到将代码拆分红文件后,this是指向那个文件的,并无指向当前组件实例,固然能够将this做为参数传进去,但我以为这样并不妥,用到一个方法就传一个this进去,会产生不少冗余代码,所以这个方案被我pass了。api
前一个方案由于this的问题以失败了结,在Vue2.x的时候官方提供了mixins来解决this问题,咱们使用mixin来定义咱们的函数,最后使用mixins进行混入,这样就能够在任意地方使用了。
因为mixins是全局混入的,一旦有重名的mixin原来的就会被覆盖,因此这个方案也不合适,pass。
上述两个方案都不合适,那 么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文件夹,将拆分出来的功能代码按组件进行划分
将每个组件中的函数进一步按功能进行细分,此处我分了四个文件夹出来
在主入口文件夹中建立InitData.ts
文件,该文件用于保存、共享当前组件须要用到的响应式data变量
全部函数拆分完成后,咱们在组件中将其导入,在setup中进行return便可
接下来咱们将上述思路进行实现。
咱们在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>
复制代码
咱们在src下建立module文件夹,用于存放咱们拆分出来的功能代码文件。
以下所示,为我建立好的目录,个人划分依据是将相同类别的文件放到一块儿,每一个文件夹的所表明的含义已在实现思路进行说明,此处不做过多解释。
咱们将组件中用到的响应式数据,统一在这里进行定义,而后在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函数,访问里面存储的变量了。
我将页面内全部的事件监听也拆分红了文件,放在了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进行操做,在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>
复制代码
在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 + "上线"
});
复制代码
我重构的websocket插件是将监听消息接收方法放在options上的,须要经过this.$options.xxx来访问,文档翻了一圈没找到有关在setup中使用的内容,那看来是不能访问了,那么我只能选择妥协,把插件挂载在options上的方法放到globalProperties上,这样问题就解决了。
上面介绍的拆分出来的文件,采用的是export function
的写法,既然项目用上了ts,那么拆分出来的文件也彻底能够采用export class
的写法,使用class写法的代码看起来会更整洁,可读性也会提高不少。
接下来,我就以项目中的截图组件为列,跟你们演示下class写法,部分代码以下所示,完整代码请移步:screen-short/main-entrance/InitData.ts
import { ComponentInternalInstance, ref } from "vue";
import { Store } from "vuex";
const screenshortLeftPosition = ref<number>(10); // 截图框选区域距离屏幕左侧的位置
const screenshortTopPosition = ref<number>(20); // 截图框选区域距离屏幕左侧的位置
const mouseDownStatus = ref<boolean>(false); // 鼠标是否按下
const mouseX = ref<number>(0); // 鼠标的X轴位置
const mouseY = ref<number>(0); // 鼠标的Y轴位置
const mouseL = ref<number>(0); // 鼠标距离左边的偏移量
const mouseT = ref<number>(0); // 鼠标距离顶部的偏移量
// 获取截图选择框dom
const frameSelectionController = ref<HTMLDivElement | null>(null);
let emit: ((event: string, ...args: any[]) => void) | undefined; // 事件处理
// store与当前实例
let $store: Store<any> | undefined;
let currentInstance: ComponentInternalInstance | null | undefined;
// 数据是否存在
let hasData: boolean | undefined;
export default class InitData {
constructor() {
// 数据为空时则初始化数据
if (!hasData) {
// 初始化完成设置其值为true
hasData = true;
screenshortLeftPosition.value = 0;
screenshortTopPosition.value = 0;
mouseDownStatus.value = false;
mouseX.value = 0;
mouseY.value = 0;
mouseL.value = 0;
mouseT.value = 0;
}
}
/** * 设置hasData属性 * @param ststus */
public setHasData(ststus: boolean) {
hasData = ststus;
}
// 获取截图框选区域距离屏幕左侧的位置
public getScreenshortLeftPosition() {
return screenshortLeftPosition;
}
// 获取截图框选区域距离屏幕顶部的位置
public getScreenshortTopPosition() {
return screenshortTopPosition;
}
/** * 设置父组件传递的数据 * @param emitParam */
public setPropsData(emitParam: (event: string, ...args: any[]) => void) {
emit = emitParam;
}
/** * 设置实例属性 * @param storeParam * @param instanceParam */
public setProperty( storeParam: Store<any>, instanceParam: ComponentInternalInstance | null ) {
$store = storeParam;
currentInstance = instanceParam;
}
}
复制代码
随后,在setup中使用new
关键词实例化后便可调用class中的public
方法,代码以下所示:
<template>
<teleport to="body">
<div id="screenshortContainer">
<div
class="frame-selection-panel"
ref="frameSelectionController"
:style="{
top: topPosition + 'px',
left: leftPosition + 'px'
}"
></div>
</div>
</teleport>
</template>
<script lang="ts">
import initData from "@/module/screen-short/main-entrance/InitData";
import eventMonitoring from "@/module/screen-short/main-entrance/EventMonitoring";
import { SetupContext } from "@vue/runtime-core";
export default {
name: "screen-short",
props: {},
setup(props: Record<string, any>, context: SetupContext<any>) {
const data = new initData();
const leftPosition = data.getScreenshortLeftPosition();
const topPosition = data.getScreenshortTopPosition();
const frameSelectionController = data.getFrameSelectionController();
new eventMonitoring(props, context as SetupContext<any>);
return {
leftPosition,
topPosition,
frameSelectionController
};
}
};
</script>
复制代码
如上所述,咱们使用到了getCurrentInstance
和useStore
,这两个内置方法还有initData中定义的那些响应式数据,只有当拆分出来的文件在setup中使用且在同步代码中才能拿到数据,不然就是null,一开始我这里说的不严谨,我在debug问题时,发现了拆分出来的文件必须在setup里调用才能拿到这些内置方法所返回的数据,写文章时就写了内置方法方法只有在setup中使用时才能拿到数据
可能我表达能力有问题,被评论区的掘友误解了😂,感谢评论区掘友@4Ark指出我这里的问题所在,他从源码的角度分析了为何会出现这个问题,从他的分析文章中我还知道了其在异步方法中调用时也拿不到数据,对此问题感兴趣的开发者能够移步至他的分析文章:从 Composition API 源码分析 getCurrentInstance() 为什么返回 null。
个人文件是拆分出去的,有些函数是运行在某个拆分出来的文件中的,不可能都在setup中执行一遍的,响应式变量也不可能全看成参数进行传递的,为了解决这个问题,我有试过使用provide
注入而后经过inject
访问,结果运行后发现很差使,控制台报黄色警告说provide
和inject
只能运行在setup
中,我直接裂开,当时发了一条沸点求助了下,到了晚上也没获得解决方案😪。
通过一番求助后,个人好友@前端印象给我提供了一个思路,成功的解决了这个问题,也就是我上面initData
的作法,将响应式变量定义在导出函数的外面,这样咱们在拆分出来的文件中导入initData
方法时,里面的变量都是指向同一个地址,能够直接访问存储在里面的变量且不会将其进行初始化。
至于getCurrentInstance
和useStore
访问出现null的情景,还有props、emit的使用问题,咱们能够在initData
的导出函数内部定义set方法,在setup里执行的方法中获取到实例后,经过set方法将其设置进咱们定义的变量中。
至此,问题就完美解决了,最后跟你们看下优化后的组件代码,393行😁
项目地址:chat-system-github
在线体验地址:chat-system