предисловие
В моем open source проекте есть компонент, который используется для отправки сообщений и отображения сообщений.Логика этого компонента очень сложная и является душой всего моего проекта.Однофайловый код имеет более 1100 строк. Каждый раз, когда я редактирую этот файл с помощью webstorm, температура процессора компьютера резко возрастает и останавливается.
Буквально несколько дней назад я, наконец, не мог ничего с собой поделать.Я понял недостатки Vue2 optionsAPI и решил использовать CompositionAPI Vue3 для решения этой проблемы.В этой статье я поделюсь с вами ямами, на которые я наступил в процессе оптимизации. и то, что я использовал Решение, приглашаю всех заинтересованных разработчиков прочитать эту статью.
анализ проблемы
Давайте сначала посмотрим на общую структуру кода компонента, как показано на следующем рисунке:
- Шаблон часть занимает 267 строк
- Сценарий, занятый 889 линиями
- Раздел стилей занимает 1 строку для внешних ссылок
Виной всему скриптовая часть, именно эту часть кода и предстоит оптимизировать в этой статье, давайте подробнее рассмотрим структуру кода в скрипте:
- Раздел реквизита занимает 6 строк
- Раздел данных занимает 52 строки
- Создаваемая часть занимает 8 строк
- Монтируемая часть занимает 98 строк
- Раздел методов занимает 672 строки.
- Секция emits занимает 6 строк.
- Вычисляемая часть занимает 8 строк
- Часовая часть занимает 26 строк
Теперь виновником является часть методов, поэтому нам нужно разделить только код части методов, и объем кода в одном файле значительно уменьшится.
Оптимизация
После приведенного выше анализа мы уже знаем проблему.Далее я поделюсь с вами решением, которое я придумал в начале, и решением, которое я, наконец, принял.
прямо в файлы
Сначала я подумал, что так как метод методов занимает слишком много строк, я создал папку методов в src, разделил методы в каждом компоненте в соответствии с именем компонента, создал соответствующую папку и создал соответствующую папку в соответствующем компоненте. , Внутри папки методы в методах разбиваются на независимые файлы ts, и, наконец, файл index.ts создается и экспортируется унифицированным образом.При использовании в компонентах модули, представленные в index.ts, импортируются по мере необходимости, как показано на следующем рисунке.
- Создать папку методов
- Разделите методы в каждом компоненте в соответствии с именем компонента и создайте соответствующую папку, а именно: message-display
- Разделите методы в методах на отдельные файлы ts, а именно: файлы ts в папке message-display
- Создайте файл index.ts, то есть файл index.ts под методами
код index.ts
Как показано ниже, мы импортируем метод разделения модуля, а затем экспортируем его единообразно.
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 в метод будет генерировать много избыточного кода, поэтому это решение было передано мной.
Используйте примеси
Предыдущее решение закончилось неудачей из-за этой проблемы и было официально представлено в Vue2.x.mixinsЧтобы решить эту проблему, мы используем примеси для определения наших функций и, наконец, используем примеси для подмешивания, чтобы их можно было использовать где угодно.
Так как миксины глобально смешаны, как только появится миксин с таким же именем, оригинальный будет перезаписан, так что это решение не подходит, проходите.
Использование API композиции
Вышеуказанные два решения не подходят, тогда CompositionAPI просто компенсирует недостатки вышеуказанных решений и успешно выполняет требования, которых мы хотим достичь.
Давайте посмотрим, чтоCompositionAPI, как описано в документе, мы можем сгруппировать функции, определенные в исходном API-интерфейсе options, и переменные данных, которые эта функция должна использовать вместе, и поместить их в функцию настройки.После завершения разработки функции поместите функции и данные, необходимые для компонент в настройке возвращается.
setupФункция выполняется до создания компонента, поэтому у нее нет этого.Эта функция может получать 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
Моему компоненту нужно получить значение в реквизитах, переданных родительским компонентом, и ему нужно передать данные родительскому компоненту через эммит.Два параметра реквизита и контекста просто решают мою проблему.
Настройка снова является функцией, что означает, что мы можем разделить все функции на отдельные файлы ts, импортировать их в компонент и вернуть в компонент в настройке, что является идеальной реализацией.Разделение, упомянутое в начале.
Реализовать идеи
Дальнейшее будет включатьОтзывчивый API, если вы не знакомы с адаптивным API, сначала перейдите к официальной документации.
После того, как мы проанализируем план, давайте взглянем на конкретный путь реализации:
-
Добавить в объект экспорта компонента
setup
Свойства, входящие реквизиты и контекст -
Создайте папку модуля в src и разделите код разделенной функции на компоненты.
-
Функции в каждом компоненте далее подразделяются по функциям, здесь я разделил их на четыре папки.
- общедоступные методы common-methods, хранящие методы, которые не должны зависеть от экземпляров компонентов
- компоненты-методы методы компонента, в которых хранятся методы, которые должен использовать текущий шаблон компонента
- главный вход главный вход, в котором хранятся функции, используемые при настройке
- Метод split-method используется для хранения методов, зависящих от экземпляров компонента, сюда же помещаются файлы, разделенные функцией в настройке.
-
Создается в основной папке входа
InitData.ts
файл, который используется для сохранения и обмена переменными адаптивных данных, которые должен использовать текущий компонент. -
После того, как все функции разделены, мы импортируем их в компонент и возвращаем в сетап.
Процесс реализации
Далее мы будем реализовывать вышеизложенные идеи.
добавить параметры настройки
В разделе экспорта компонента vue мы добавляем параметр настройки внутри его объекта следующим образом:
<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 для хранения файлов кода функций, которые мы выделили.
Как показано ниже, каталог, созданный для меня, основан на объединении файлов одной категории. Значение каждой папки было объяснено в идее реализации, поэтому я не буду объяснять это здесь слишком подробно.
Создайте файл InitData.ts
Мы определяем отзывчивые данные, используемые в компоненте здесь, а затем возвращаем их в настройке.Часть кода в файле определяется следующим образом, пожалуйста, перейдите к полному коду: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
}
}
⚠️ Внимательные разработчики могли обнаружить, что я определил переменную Response вне экспортируемой функции. Причина этого в каких-то особых причинах настройки. Я подробно объясню, почему я это делаю, в следующей главе о подводных камнях.
использовать в компоненте
После определения соответствующей мертвой переменной мы можем импортировать и использовать ее в компоненте.Часть кода выглядит следующим образом, перейдите к полному коду: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, и когда вам нужно изменить ее значение, вам нужно изменить только ее значение.
До сих пор вам было объяснено основное использование API композиции, и я поделюсь с вами ямами, на которые я наступил в процессе реализации, и своим решением.
Ступайте на яму, чтобы поделиться
Сегодня четверг, я решил использовать CompositionAPI для рефакторинга моего компонента в понедельник, и только вчера вечером рефакторинг был завершен, и я наступил на много ям. Это все еще имеет смысл 😎.
Далее я поделюсь с вами некоторыми ямами, на которые я наступил, и своими решениями.
дом операция
Мой компонент должен работать с dom, который можно использовать в optionsAPI.this.$refs.xxx
Для доступа к компоненту dom в настройке этого нет.Полистав официальную документацию, я обнаружил, что его нужно определить по 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
Доступ к vuex в настройках должен осуществляться через useStore(), код выглядит следующим образом:
import { useStore } from "vuex";
const $store = useStore();
console.log($store.state.token);
получить доступ к текущему экземпляру
В компоненте вам нужно получить доступ к монтированию вglobalProperties
Вышеуказанные вещи должны быть переданы в настройке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
Refactored Webactocket Plug-In Refactored поставьте метод приема сообщения мониторинга в параметрах, которые необходимо получить через это. $ Options.xxx. Я включил в документацию и не смог найти содержимое, используемую в настройке. Кажется, Что оно не может быть доступен. Тогда я могу выбрать только компромисс и поместить метод монтажа плагина на параметры по глобальным способам, и проблема будет решена.
Написание классов для разделенных файлов
Разделенные файлы, описанные выше, используютexport function
Способ написания, так как в проекте используется ts, можно использовать и разделенные файлыexport 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;
}
}
Затем в настройке используйтеnew
После создания экземпляра ключевого слова класс в классе может быть вызван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中使用时才能拿到数据
Возможно, у меня проблемы с самовыражением, и меня неправильно поняли друзья в области комментариев 😂, спасибо друзьям в области комментариев@4ArkУкажите проблему здесь.Он проанализировал, почему эта проблема возникает с точки зрения исходного кода.Из его аналитической статьи я также знаю, что он не может получить данные, когда он вызывается в асинхронном методе.Заинтересованы в развитии этой проблемы Читатели можно перейти к его аналитической статье:Анализ того, почему getCurrentInstance() возвращает null из исходного кода Composition API.
Мои файлы разделены, и некоторые функции запускаются в разделенном файле. Невозможно выполнить их все в настройке. Также невозможно передать все отзывчивые переменные в качестве параметров. Чтобы решить эту проблему, я попытался с использованиемprovide
вводить, а затем передаватьinject
Access, в результате после запуска его неудобно использовать, и консоль сообщает о желтом предупреждении о том, чтоprovide
а такжеinject
работает только наsetup
, я взломал напрямую, а потом выложилточка кипенияЯ обратился за помощью, но решения ночью так и не получил 😪.
После некоторой помощи, мой друг@внешнее впечатлениеПодал мне идею и успешно решил эту задачу, т.е. надо мнойinitData
метод, определяем переменную Response вне функции экспорта, чтобы мы импортировали ее в разделенный файлinitData
метод, все переменные в нем указывают на один и тот же адрес, и к переменным, хранящимся в нем, можно получить прямой доступ без их инициализации.
Что касаетсяgetCurrentInstance
а такжеuseStore
Доступ к нулевым сценариям, а также использование props и emit мы можемinitData
Метод set определяется внутри функции экспорта .После того, как экземпляр получен в методе, выполняемом в настройке, он устанавливается в переменную, которую мы определили с помощью метода set.
На данный момент проблема решена отлично, наконец, давайте посмотрим на оптимизированный код компонента, 393 строки 😁
адрес проекта
адрес проекта:chat-system-github
Адрес онлайн-опыта:chat-system
напиши в конце
- Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь 😊
- Эта статья была впервые опубликована на Наггетс, перепечатка без разрешения запрещена 💌