使用Vue3的CompositionAPI來優(yōu)化代碼量
本文轉載自微信公眾號「神奇的程序員k」,作者神奇的程序員k 。轉載本文請聯(lián)系神奇的程序員k公眾號。
前言
在我的開源項目中有一個組件是用來發(fā)送消息和展示消息的,這個組件的邏輯很復雜也是我整個項目的靈魂所在,單文件代碼有1100多行。我每次用webstorm編輯這個文件時,電腦cpu溫度都會飆升并伴隨著卡頓。
就在前幾天我終于忍不住了,意識到了Vue2的optionsAPI的缺陷,決定用Vue3的CompositionAPI來解決這個問題,本文就跟大家分享下我在優(yōu)化過程中踩到的坑以及我所采用的解決方案,歡迎各位感興趣的開發(fā)者閱讀本文。
問題分析
我們先來看看組件的整體代碼結構,如下圖所示:
image-20210114095802363
- template部分占用267行
- script部分占用889行
- style部分為外部引用占用1行
罪魁禍首就是script部分,本文要優(yōu)化的就是這一部分的代碼,我們再來細看下script中的代碼結構:
- props部分占用6行
- data部分占用52行
- created部分占用8行
- mounted部分占用98行
- methods部分占用672行
- emits部分占用6行
- computed部分占用8行
- watch部分占用26行
現(xiàn)在罪魁禍首是methods部分,那么我們只需要把methods部分的代碼拆分出去,單文件代碼量就大大減少了。
優(yōu)化方案
經過上述分析后,我們已經知道了問題所在,接下來就跟大家分享下我一開始想到的方案以及最終所采用的方案。
直接拆分成文件
一開始我覺得既然methods方法占用的行數(shù)太多,那么我在src下創(chuàng)建一個methods文件夾,把每個組件中的methods的方法按照組件名進行劃分,創(chuàng)建對應的文件夾,在對應的組件文件夾內部,將methods中的方法拆分成獨立的ts文件,最后創(chuàng)建index.ts文件,將其進行統(tǒng)一導出,在組件中使用時按需導入index.ts中暴露出來的模塊,如下圖所示:
image-20210114103824562
- 創(chuàng)建methods文件夾
- 把每個組件中的methods的方法按照組件名進行劃分,創(chuàng)建對應的文件夾,即:message-display
- 將methods中的方法拆分成獨立的ts文件,即:message-display文件夾下的ts文件
- 創(chuàng)建index.ts文件,即:methods下的index.ts文件
index.ts代碼
如下所示,我們將拆分的模塊方法進行導入,然后統(tǒng)一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();
- }
- })
運行結果
當我自信滿滿的開始跑項目時,發(fā)現(xiàn)瀏覽器的控制臺報錯了,提示我this未定義,突然間我意識到將代碼拆分成文件后,this是指向那個文件的,并沒有指向當前組件實例,當然可以將this作為參數(shù)傳進去,但我覺得這樣并不妥,用到一個方法就傳一個this進去,會產生很多冗余代碼,因此這個方案被我pass了。
使用mixins
前一個方案因為this的問題以失敗告終,在Vue2.x的時候官方提供了mixins來解決this問題,我們使用mixin來定義我們的函數(shù),最后使用mixins進行混入,這樣就可以在任意地方使用了。
由于mixins是全局混入的,一旦有重名的mixin原來的就會被覆蓋,所以這個方案也不合適,pass。
image-20210114111746208
使用CompositionAPI
上述兩個方案都不合適,那 么CompositionAPI就剛好彌補上述方案的短處,成功的實現(xiàn)了我們想要實現(xiàn)的需求。
我們先來看看什么是CompositionAPI,正如文檔所述,我們可以將原先optionsAPI中定義的函數(shù)以及這個函數(shù)需要用到的data變量,全部歸類到一起,放到setup函數(shù)里,功能開發(fā)完成后,將組件需要的函數(shù)和data在setup進行return。
setup函數(shù)在創(chuàng)建組件之前執(zhí)行,因此它是沒有this的,這個函數(shù)可以接收2個參數(shù): 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來向父組件傳遞數(shù)據,props和context這兩個參數(shù)正好解決了我這個問題。
setup又是個函數(shù),也就意味著我們可以將所有的函數(shù)拆分成獨立的ts文件,然后在組件中導入,在setup中將其return給組件即可,這樣就很完美的實現(xiàn)了一開始我們一開始所說的的拆分。
實現(xiàn)思路
接下來的內容會涉及到響應性API,如果對響應式API不了解的開發(fā)者請先移步官方文檔。
我們分析出方案后,接下來我們就來看看具體的實現(xiàn)路:
- 在組件的導出對象中添加setup屬性,傳入props和context
- 在src下創(chuàng)建module文件夾,將拆分出來的功能代碼按組件進行劃分
- 將每一個組件中的函數(shù)進一步按功能進行細分,此處我分了四個文件夾出來
- common-methods 公共方法,存放不需要依賴組件實例的方法
- components-methods 組件方法,存放當前組件模版需要使用的方法
- main-entrance 主入口,存放setup中使用的函數(shù)
- split-method 拆分出來的方法,存放需要依賴組件實例的方法,setup中函數(shù)拆分出來的文件也放在此處
- 在主入口文件夾中創(chuàng)建InitData.ts文件,該文件用于保存、共享當前組件需要用到的響應式data變量
- 所有函數(shù)拆分完成后,我們在組件中將其導入,在setup中進行return即可
實現(xiàn)過程
接下來我們將上述思路進行實現(xiàn)。
添加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>
創(chuàng)建module模塊
我們在src下創(chuàng)建module文件夾,用于存放我們拆分出來的功能代碼文件。
如下所示,為我創(chuàng)建好的目錄,我的劃分依據是將相同類別的文件放到一起,每個文件夾的所代表的含義已在實現(xiàn)思路進行說明,此處不作過多解釋。
創(chuàng)建InitData.ts文件
我們將組件中用到的響應式數(shù)據,統(tǒng)一在這里進行定義,然后在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中的數(shù)據寫入當前實例
- 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
- }
- }
??細心的開發(fā)者可能已經發(fā)現(xiàn),我把響應式變量定義在導出的函數(shù)外面了,之所以這么做是因為setup的一些特殊原因,在下面的踩坑章節(jié)我將會詳解我為什么要這樣做。
在組件中使用
定義完相應死變量后,我們就可以在組件中導入使用了,部分代碼如下所示,完整代碼請移步:message-display.vue
- import initData from "@/module/message-display/main-entrance/InitData";
- export default defineComponent({
- setup(props, context) {
- // 初始化組件需要的data數(shù)據
- 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函數(shù),訪問里面存儲的變量了。
在文件中訪問initData
我將頁面內所有的事件監(jiān)聽也拆分成了文件,放在了EventMonitoring.ts中,在事件監(jiān)聽的處理函數(shù)是需要訪問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();
- // 獲取傳遞的參數(shù)
- const data = initData();
- // 將props改為響應式
- const prop = toRefs(props);
- // 獲取data中的數(shù)據
- 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>;
- // 監(jiān)聽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發(fā)生改變,清空消息列表數(shù)據
- senderMessageList.length = 0;
- // 初始化分頁數(shù)據
- 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的基本使用就跟大家講解完了,下面將跟大家分享下我在實現(xiàn)過程中所踩的坑,以及我的解決方案。
踩坑分享
今天是周四,我周一開始決定使用CompositionAPI來重構我這個組件的,一直搞到昨天晚上才重構完成,前前后后踩了很多坑,正所謂踩坑越多你越強,這句話還是很有道理的??。
接下來就跟大家分享下我踩到的一些坑以及我的解決方案。
dom操作
我的組件需要對dom進行操作,在optionsAPI中可以使用this.$refs.xxx來訪問組件dom,在setup中是沒有this的,翻了下官方文檔后,發(fā)現(xiàn)需要通過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插件是將監(jiān)聽消息接收方法放在options上的,需要通過this.$options.xxx來訪問,文檔翻了一圈沒找到有關在setup中使用的內容,那看來是不能訪問了,那么我只能選擇妥協(xié),把插件掛載在options上的方法放到globalProperties上,這樣問題就解決了。
內置方法只能在setup中訪問
如上所述,我們使用到了getCurrentInstance和useStore,這兩個內置方法還有initData中定義的那些響應式數(shù)據,只有在setup中使用時才能拿到數(shù)據,否則就是null。
我的文件是拆分出去的,有些函數(shù)是運行在某個拆分出來的文件中的,不可能都在setup中執(zhí)行一遍的,響應式變量也不可能全當作參數(shù)進行傳遞的,為了解決這個問題,我有試過使用provide注入然后通過inject訪問,結果運行后發(fā)現(xiàn)不好使,控制臺報黃色警告說provide和inject只能運行在setup中,我直接裂開,當時發(fā)了一條沸點求助了下,到了晚上也沒得到解決方案??。
經過一番求助后,我的好友@前端印象給我提供了一個思路,成功的解決了這個問題,也就是我上面initData的做法,將響應式變量定義在導出函數(shù)的外面,這樣我們在拆分出來的文件中導入initData方法時,里面的變量都是指向同一個地址,可以直接訪問存儲在里面的變量且不會將其進行初始化。
至于getCurrentInstance和useStore訪問出現(xiàn)null的情景,還有props、emit的使用問題,我們可以在initData的導出函數(shù)內部定義set方法,在setup里的方法中獲取到實例后,通過set方法將其設置進我們定義的變量中。
至此,問題就完美解決了,最后跟大家看下優(yōu)化后的組件代碼,393行??
圖片
image-20210114201837539
項目地址
項目地址:chat-system-github
在線體驗地址:chat-system