Как написать композитный элемент Vue шаг за шагом
Вы слышали о композитных элементах Vue и хотите написать свой собственный? Возможно, вы даже использовали композиции, написанные другими, но не уверены, как начать создавать их самостоятельно. Именно об этом и пойдет речь в этой статье!
Что такое композитный элемент Vue.js?
Для начала давайте вкратце поговорим о том, что такое композитный элемент Vue. Композитный элемент Vue похож на утилиту или вспомогательную функцию, но с одним важным отличием: он имеет состояние. То есть он включает данные, определенные с помощью функции Vue reactive
или ref
.
Пример, используемый в документации Vue.js, – это композитная функция useMouse
. Идея заключается в том, что данные x и y, отображаемые композитом, являются реактивными. Это означает, что всякий раз, когда данные обновляются, они вызывают повторный рендеринг соответствующих элементов DOM. Здесь вы можете увидеть, каким будет эффект от композитного элемента useMouse
.
Прелесть композитных элементов в том, что мы можем воспользоваться преимуществами определения реактивных данных (как в компоненте), но при этом нам не нужно ничего рендерить! Это означает, что мы можем абстрагировать логику с состоянием вне компонента и повторно использовать ее в контексте различных компонентов (или даже без какого-либо компонента).
Вы можете получить хорошее представление о том, что возможно, изучив документацию VueUse. Это коллекция из более чем 200 устанавливаемых и мгновенно используемых компонент, которые решают такие распространенные проблемы, как работа с данными в localStorage, переключение режима свет/темнота и многое другое.
Определение композитного элемента Vue
Теперь, когда вы имеете представление о том, что такое композитный элемент, давайте шаг за шагом пройдемся по его созданию.
Первым шагом будет создание файла, в котором будет жить ваш компонент. Обычно это делается в директории под названием composables
.
В этот файл вы экспортируете функцию с именем, которое описывает, что делает ваш компонент. В этой статье мы создадим композитную функцию useCycleList в качестве примера. Вы можете увидеть проверенную и верную реализацию композитного списка useCycleList из библиотеки VueUse. Ознакомьтесь с документацией по ней, чтобы понять, что она делает.
// @/src/composables/useCyleList.ts
export const useCycleList = ()=>{}
Префикс use для компонентов Vue
Обратите внимание, что имя композитного элемента начинается с use
. Это также еще одно общее соглашение, помогающее пользователям вашей композитной функции отличить ее от обычной нестационарной вспомогательной функции.
Рассмотрим написание составных элементов на TypeScript
Также очень хорошей идеей является написание компонентов на TypeScript. Это делает их более интуитивно понятными в использовании (отличное автозаполнение, обнаружение ошибок и т. д.). Именно так мы и поступим в этой статье, но если вам не очень удобно работать с TS, можно писать компоненты на обычном JS.
Приём составных аргументов
Не все композитные элементы требуют аргумента, но большинство – да. Для нашего примера возьмем массив. Это список, который мы хотим перебрать. Пока что это будет массив элементов любого типа.
// if not using TS you can remove the :any[]
export const useCycleList = (list: any[])=>{}
Аргументы образуют интерфейс (или API) для ввода данных в компонент.
Возврат данных и функций из композитного элемента
Далее давайте определим API вывода данных нашей композитной функции, то есть то, что возвращается из функции.
export const useCycleList = (list: any[])=>{
return {
prev,
next,
state
}
}
Здесь мы видим 3 вещи. Давайте разберем каждую из них.
state
Это будут реактивные данные. Это один элемент в массиве. Это "активный" элемент в списке, по которому мы перебираем данные.
Например, при следующем использовании state
изначально будет Dog
(первый элемент в массиве).
const { state } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
console.log(state) // Dog
next
Функция next
позволяет пользователю перейти к следующему элементу списка. Таким образом, при следующем коде state
будет Cat
.
const { state, next } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
next()
console.log(state) // Cat
prev
Функция prev
позволяет двигаться по списку в обратном направлении. Например:
const { state, prev } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
prev()
console.log(state) // Lizard
prev()
console.log(state) // Cat
Рабочий процесс создания композитных API
Обратите внимание, что сначала мы определили интерфейс нашего компонента (его API). Пока ничего из этого не работает, потому что мы не реализовали логику. Но это не страшно. На самом деле это очень хороший рабочий процесс для написания композита. Определите, как вы хотите его использовать (сделайте его действительно красивым), до того, как вы реализуете детали.
Такой подход к проектированию применим ко всем видам кода (компоненты, хранилища и т. д.), но мы определенно можем применить его и к композитным элементам.
Определите реактивное состояние для компонента
Теперь давайте создадим реактивное состояние, чтобы следить за тем, какой элемент в массиве "активен". Это то, что делает его композитным и действительно то, что делает его полезным в контексте приложения Vue. Что нам действительно нужно, так это позиция активного в данный момент элемента. Поэтому давайте создадим реактивную переменную activeIndex
.
import { ref } from "vue";
export const useCycleList = (list: any[])=>{
// 👇 Define a ref to keep track of the active index
const activeIndex = ref(0);
//...
}
Затем мы можем создать некоторые реактивные производные данные (т.е. «computed ref»), чтобы определить, какой state
будет основан на значении activeIndex
.
import { ref, computed } from "vue";
export const useCycleList = (list: any[])=>{
const activeIndex = ref(0);
// 👇 reactive `state` is based on the activeIndex
const state = computed(()=> list[activeIndex.value]);
//...
return { state /*...*/ }
}
Определение функций Exposed для компонентов
Таким образом, функции prev
и next
очень легко реализовать.
export const useCycleList = (list: any[])=>{
//...
// 👇 the next function
function next() {
// if the `state` is the last item, start from the beginning of the list
if (activeIndex.value === list.length - 1) {
activeIndex.value = 0;
} else {
// otherwise just increment the activeIndex by 1
activeIndex.value += 1;
}
}
// 👇 the prev function
function prev() {
// if the `state` is the first item, wrap to end end
if (activeIndex.value === 0) {
activeIndex.value = list.length - 1;
} else {
// otherwise just decrement the activeIndex by 1
activeIndex.value -= 1;
}
}
//...
}
Разрешение компонентам принимать реактивные аргументы
При написании компонента необходимо учитывать один важный момент: люди часто работают с реактивными данными в своих компонентах и ожидают, что смогут интуитивно передавать эти реактивные данные в любую компонентную часть.
Другими словами, они могут захотеть сделать следующее:
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(list);
Поэтому давайте обновим компонент, чтобы он мог принимать реактивные списки (списки, определенные с помощью ref
или reactive
).
import { ref, computed, watch, type Ref } from 'vue';
// 👇 now we're accepting a ref that is an array of anything
export const useCycleList = (list: Ref<any[]>) => {
//...
// And then throughout the composable, you'll need to replace all uses of
// `list` with `list.value`
// for example 👇
const state = computed(() => list.value[activeIndex.value]);
// do the same for list in next, prev, etc...
// 👇 finally, since the list can change now
// let's run a little cleanup on the activeIndex
// if the list is changed out to something shorter
// than the activeIndex
watch(list, () => {
if (activeIndex.value >= reactiveList.value.length) {
activeIndex.value = 0;
}
});
// ...
};
Разрешение компонентам принимать нереактивные и реактивные аргументы (и геттеры!)
Вышеописанное отлично подходит для приема реактивного списка. Но теперь мы обязали пользователя композитных элементов передавать что-то реактивное. Мне нравилась простота передачи обычного массива, когда это было необходимо. Не волнуйтесь! Мы можем поддерживать оба варианта с помощью вспомогательной функции Vue под названием toRef
.
import { ref, computed } from "vue";
import { type MaybeRefOrGetter, toRef, watch } from "vue";
// 👇 notice we're using the `MaybeRefOrGetter` type from Vue core
export const useCycleList = (list: MaybeRefOrGetter<any[]>) => {
// calling toRef normalizes the list to a ref (no matter how it was passed)
const reactiveList = toRef(list);
// replace all uses of list.value
// with reactiveList.value
// for example 👇
const state = computed(() => reactiveList.value[activeIndex.value]);
// do the same for list in next, prev, watch, etc...
//...
}
Теперь мы можем поддерживать оба типа данных для этого списка, плюс мы получаем поддержку третьего типа. Теперь работают все следующие типы:
// As plain data
const { state, prev, next } = useCycleList(
['Dog', 'Cat', 'Lizard']
);
// As reactive data
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(list);
// As a getter
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(()=> list.value);
Улучшение API композитных элементов с помощью записываемого вычисляемого реквизита
В настоящее время, если мы попытаемся установить state
из компонента, это не сработает. Почему? Мы определяем его как вычисляемый реквизит.
const state = computed(() => reactiveList.value[activeIndex.value]);
Чтобы сделать API немного более гибким, я думаю, имеет смысл, чтобы запись в state
обновляла базовый элемент в массиве. Нет проблем! Vue справится и с этим!
const state = computed({
// the get method is called when state is read
get() {
// this is the same as the return from the function before
return reactiveList.value[activeIndex.value];
},
// the set method is called when state is written to
set(value) {
// take the given value and apply it to the array item
// at the currently active index
reactiveList.value[activeIndex.value] = value;
},
});
Это не 100% необходимая функция, но стоит подумать о том, как все пользователи могут использовать ваш компонент, и сделать его максимально интуитивно понятным.
Создание безопасного компонента
До сих пор мы использовали any[]
для определения аргумента list
. Это вполне логично, поскольку мы хотим, чтобы наш компонент работал для массива чего угодно (а не только для строк, как в примерах).
Однако большинство пользователей TS знают, что использование any
– это небольшой раздражитель в коде. Его наличие означает, что есть место для улучшения.
Поэтому давайте воспользуемся общим выражением, чтобы сказать, что каждый элемент массива является переменной некоторого типа.
// T is the generic here
export const useCycleList = <T>(list: MaybeRefOrGetter<T[]>) => {
Чем это полезно?
Теперь TypeScript может сделать вывод, что наше состояние – это WritableComputedRef
того же типа, что и элементы переданного списка. В нашем примере это строка.
Вот скриншот того, как все выглядит в моей IDE до использования общего типа (то есть использования any
).
Вот как это выглядит после внедрения общего типа.
А если мы предоставим другой тип данных для каждого элемента в списке:
Заключение
Композитные элементы – это мощный инструмент для создания многократно используемой логики с учетом состояния в ваших приложениях Vue.js. Их легче написать, чем вы думаете. Хотя VueUse предоставляет вам огромное количество готовых композиций, бывает, что вам нужно написать свою собственную. Теперь вы знаете, как это сделать!
Посмотрите полную реализацию нашего пользовательского композита useCycleList
здесь.