Библиотека зрелых компонентов обычно состоит из десятков часто используемых компонентов пользовательского интерфейса, включая базовые компоненты, такие как кнопки (Button) и поля ввода (Input), а также таблицы (Table), средства выбора даты (DatePicker), карусели (Carousel) и другие автономные сложные компоненты.
Здесь мы предлагаемСложность компонентаОсновным источником сложности компонента является его собственное состояние, то есть сколько состояний должен поддерживать сам компонент, не зависящий от внешнего ввода. Ссылаясь на тупой компонент и интеллектуальный компонент, упомянутые в исходной статье, разница между ними заключается в том, нужно ли вам поддерживать состояние внутри компонента, которое не зависит от внешнего ввода.
Практический пример — компонент карусели
В этой статье мы возьмем компонент карусели в качестве примера, чтобы шаг за шагом восстановить, как реализовать плавный интерактивный компонент карусели.
Самый простой компонент карусели
Отбрасывая все сложные функции, суть компонента карусели фактически заключается в переключении между различными элементами в фиксированной области. После выяснения этого мы можем спроектировать базовую DOM-структуру карусельного компонента следующим образом:
<Frame>
<SlideList>
<SlideItem />
...
<SlideItem />
</SlideList>
</Frame>
Как показано ниже:
Frame
То есть реальная область отображения карусельного компонента, а также его ширина и высота вводятся пользователем внутренне.SlideItem
Принимать решение. Здесь следует отметить, что вам нужно установитьFrame
изoverflow
собственностьhidden
, то есть скрыть часть, превышающую его собственную ширину и высоту, и отображать только по одномуSlideItem
.
SlideList
Для контейнера дорожки карусельного компонента измените егоtranslateX
Значение , может быть достигнуто путем скольжения по дорожке для отображения различных элементов карусели.
SlideItem
Это уровень абстракции для элемента карусели, вводимого пользователем, и внутренний может бытьimg
илиdiv
и другие элементы DOM, не влияет на логику самого компонента карусели.
Реализуйте переключатель перед элементом карусели
Для достижения различныхSlideItem
Чтобы переключаться между ними, нам нужно определить первое внутреннее состояние компонента карусели, а именноcurrentIndex
, то есть текущий отображаемый элемент каруселиindex
стоимость.上文中我们提到了改变SlideList
изtranslateX
является ключом к реализации переключения элементов карусели, поэтому здесь нам нужноcurrentIndex
а такжеSlideList
изtranslateX
Соответственно, то есть:
translateX = -(width) * currentIndex
width
ширина одного элемента карусели, такая же, какFrame
имеют одинаковую ширину, поэтому мы можемcomponentDidMount
время, чтобы получитьFrame
, и используйте это для расчета общей ширины дорожки.
componentDidMount() {
const width = get(this.container.getBoundingClientRect(), 'width');
}
render() {
const rest = omit(this.props, Object.keys(defaultProps));
const classes = classnames('ui-carousel', this.props.className);
return (
<div
{...rest}
className={classes}
ref={(node) => { this.container = node; }}
>
{this.renderSildeList()}
{this.renderDots()}
</div>
);
}
На данный момент нам нужно только изменить компонент карусели вcurrentIndex
, вы можете косвенно изменитьSlideList
изtranslateX
, чтобы переключаться между элементами карусели.
Реагировать на действия пользователя
Как общий общий компонент, карусель имеет очень широкий спектр приложений как на настольных, так и на мобильных терминалах.Здесь мы берем мобильный терминал в качестве примера, чтобы проиллюстрировать, как реагировать на действия пользователя.
{map(children, (child, i) => (
<div
className="slideItem"
role="presentation"
key={i}
style={{ width }}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
>
{child}
</div>
))}
На мобильной стороне нам нужно отслеживать три события, чтобы реагировать на начало слайда, слайд до конца и слайд до конца. Скользящее начало и скользящее окончание — это одноразовые события, а скольжение — непрерывное событие, поэтому мы можем определить, какие значения нам нужно определить в каждом из трех событий.
проведите, чтобы начать
- startPositionX: начальная позиция этого слайда
handleTouchStart = (e) => {
const { x } = getPosition(e);
this.setState({
startPositionX: x,
});
}
скользящий
- moveDeltaX: расстояние до этого слайда в реальном времени.
- направление: направление этого слайда в реальном времени
- translateX: положение дорожки в этом слайде в реальном времени, используемое для рендеринга.
handleTouchMove = (e) => {
const { width, currentIndex, startPositionX } = this.state;
const { x } = getPosition(e);
const deltaX = x - startPositionX;
const direction = deltaX > 0 ? 'right' : 'left';
this.setState({
moveDeltaX: deltaX,
direction,
translateX: -(width * currentIndex) + deltaX,
});
}
проведите до конца
- currentIndex: новый currentIndex после окончания слайда.
- endValue: translateX дорожки после окончания свайпа.
handleTouchEnd = () => {
this.handleSwipe();
}
handleSwipe = () => {
const { children, speed } = this.props;
const { width, currentIndex, direction, translateX } = this.state;
const count = size(children);
let newIndex;
let endValue;
if (direction === 'left') {
newIndex = currentIndex !== count ? currentIndex + 1 : START_INDEX;
endValue = -(width) * (currentIndex + 1);
} else {
newIndex = currentIndex !== START_INDEX ? currentIndex - 1 : count;
endValue = -(width) * (currentIndex - 1);
}
const tweenQueue = this.getTweenQueue(translateX, endValue, speed);
this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex));
}
Поскольку мы обновляем translateX трека в режиме реального времени, когда мы пролистываем, наша карусель может это сделать.следовать примерувзаимодействие с пользователем, то есть одним движением по экрану элемент карусели будет перемещаться влево или вправо в соответствии с действиями пользователя.
Анимация с коммутацией полной скольжения
После реализации пользовательского опыта следования за рукой в скольжении нам также необходимо расположить отображаемый элемент карусели на новом после окончания скольжения.currentIndex
. В зависимости от направления скольжения пользователя мы можемcurrentIndex
+1 или -1, чтобы получить новыйcurrentIndex
. Но при обработке первого элемента проведите пальцем влево или последнего элемента проведите пальцем вправо, новыйcurrentIndex
Нужно обновить до последней или первой.
Логика здесь несложная, но она приводит к очень сложной проблеме взаимодействия с пользователем, то есть предположим, что у нас есть 3 элемента карусели, каждый шириной 300 пикселей, то есть, когда отображается последний элемент, TranslateX дорожки равен -600 пикселей. После того, как мы сдвинем последний элемент влево, значение translateX дорожки будет переопределено до 0 пикселей. Если мы используем нативную анимацию CSS:
transition: 1s ease-in-out;
Трек будет скользить слева направо к первому элементу карусели в течение секунды, что противоречит здравому смыслу, потому что свайп пользователя влево вызывает правую анимацию и наоборот.
Эта проблема мучает многих фронтенд разработчиков с древних времен, я также видел следующие решения проблемы:
- Задайте ширину дорожки бесконечно длинной (несколько миллионов пикселей), бесконечно повторяя конечный элемент карусели. Это решение, очевидно, является взломом и на самом деле не решает проблему компонента карусели.
- Визуализируйте только три элемента карусели, а именно предыдущий, текущий и следующий, и обновляйте эти три элемента одновременно после каждого свайпа. Это решение очень сложно реализовать, потому что состояние, которое нужно поддерживать внутри компонента, увеличивается с одного currentIndex до трех элементов DOM с собственным состоянием, а из-за постоянного удаления и добавления узлов DOm производительность низкая.
Давайте еще раз подумаем о характере операции смахивания. За исключением первого и двух последних элементов, новое значение translateX фиксируется после скольжения всех промежуточных элементов, т. е.-(width * currentIndex)
, анимация в этом случае может быть легко достигнута идеально. И когда последний элемент сдвинется влево, потому что дорожкаtranslateX
Достигнут предел, как в такой ситуации добиться плавной анимации перехода?
Здесь мы решили соединить последний и первый элементы с началом и концом дорожки соответственно, чтобы обеспечить плавную анимацию переключения без изменения структуры DOM:
Таким образом мы объединяемся после окончания каждого свайпаendValue
метод расчета, т.
// left
endValue = -(width) * (currentIndex + 1)
// right
endValue = -(width) * (currentIndex - 1)
Высокопроизводительная анимация с помощью requestAnimationFrame
requestAnimationFrame
Это предоставляемый браузером API, предназначенный для реализации анимации. Заинтересованные друзья могут просмотреть его еще раз.«Анализ функции облегчения движения React»этот столбец.
Все анимации, по сути, представляют собой серию значений на оси времени, в частности, в сценарии карусели, то есть: начальное значение — это значение, когда пользователь перестает скользить, а новое значение — это начальное значение.currentIndex
ВремяtranslateX
Значение является конечным значением в течение времени анимации, установленного пользователем (например, 0,5 секунды), в соответствии с функцией смягчения, установленной пользователем, вычислить время анимации каждого кадра.translateX
значение и в итоге получаем массив, который обновляется со скоростью 60 кадров в секунду в дорожке.style
характеристики. Каждое обновление будет потреблять промежуточное значение в массиве значений анимации до тех пор, пока не будут использованы все промежуточные значения в массиве, анимация завершится и вызовет обратный вызов.
Конкретный код выглядит следующим образом:
const FPS = 60;
const UPDATE_INTERVAL = 1000 / FPS;
animation = (tweenQueue, newIndex) => {
if (isEmpty(tweenQueue)) {
this.handleOperationEnd(newIndex);
return;
}
this.setState({
translateX: head(tweenQueue),
});
tweenQueue.shift();
this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex));
}
getTweenQueue = (beginValue, endValue, speed) => {
const tweenQueue = [];
const updateTimes = speed / UPDATE_INTERVAL;
for (let i = 0; i < updateTimes; i += 1) {
tweenQueue.push(
tweenFunctions.easeInOutQuad(UPDATE_INTERVAL * i, beginValue, endValue, speed),
);
}
return tweenQueue;
}
В функции обратного вызова текущее новое устойчивое значение компонента определяется единообразно в соответствии с логикой изменения:
handleOperationEnd = (newIndex) => {
const { width } = this.state;
this.setState({
currentIndex: newIndex,
translateX: -(width) * newIndex,
startPositionX: 0,
moveDeltaX: 0,
dragging: false,
direction: null,
});
}
Эффект завершенного карусельного компонента следующий:
Изящно обрабатывать особые случаи
- Обработка случайных прикосновений пользователя: на мобильной стороне пользователи часто ошибочно касаются компонента карусели, даже если они случайно скользят по нему или нажимают на него.
onTouch
классное мероприятие. В связи с этим мы можем добавить пороговое значение расстояния скольжения, чтобы избежать случайных прикосновений пользователя.Порог может составлять 10% от ширины элемента карусели или другие разумные значения.Когда расстояние скольжения превышает порог, последующее наблюдение компонента карусели будет активирован слайд. - Адаптация для настольных компьютеров: для настольных компьютеров имена событий, на которые должен реагировать карусельный компонент, полностью отличаются от имен для мобильных устройств, но они могут быть соответствующим образом сопоставлены. Здесь также следует отметить, что нам нужно добавить состояние перетаскивания в компонент карусели, чтобы различать мобильные и настольные устройства, чтобы безопасно повторно использовать часть кода обработчика.
// mobile
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
// desktop
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
onMouseLeave={this.handleMouseLeave}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut}
handleMouseDown = (evt) => {
evt.preventDefault();
this.setState({
dragging: true,
});
this.handleTouchStart(evt);
}
handleMouseMove = (evt) => {
if (!this.state.dragging) {
return;
}
this.handleTouchMove(evt);
}
handleMouseUp = () => {
if (!this.state.dragging) {
return;
}
this.handleTouchEnd();
}
handleMouseLeave = () => {
if (!this.state.dragging) {
return;
}
this.handleTouchEnd();
}
handleMouseOver = () => {
if (this.props.autoPlay) {
clearInterval(this.autoPlayTimer);
}
}
handleMouseOut = () => {
if (this.props.autoPlay) {
this.autoPlay();
}
}
резюме
На данный момент мы достиглиtween-functions
Сторонний зависимый карусельный компонент, размер пакета не более 2 КБ, полный исходный код можно посмотреть здесьcarousel/index.js.
Помимо сэкономленного объема кода, нас больше радует то, что мы досконально разобрались в способе реализации карусельного компонента и в том, как его использовать.requestAnimationFrame
СотрудничатьsetState
чтобы завершить набор анимаций в реакции.
впечатление
Вышеупомянутый мультфильм должен был увидеть каждый.Он интересен и также содержит простую, но глубокую истину, то есть при решении сложной задачи самое главное - это мышление, но одного мышления еще далеко не достаточно.Конкретные планы реализации тоже есть требуется. Этот конкретный план реализации должен быть непрерывным, без каких-либо недостающих звеньев и без каких-либо скачков в мышлении или исполнении. Таким образом, нет серебряной пули или быстрого решения какой-либо сложной проблемы, мы должны понять ее, понять, и тогда мы действительно сможем ее решить.
На этом цикл статей о практике проектирования библиотек компонентов также подходит к концу. Во всех четырех статьях мы обсудили основные темы архитектуры библиотеки компонентов, классификации компонентов, организации документов, интернационализации и сложного проектирования компонентов.Из-за ограниченности возможностей автора, естественно, есть много недостатков в понимании.
Библиотека компонентов является главным приоритетом для повышения эффективности работы фронтенд-команды, и на ее изучение не так уж много времени. Кроме того, стыковка с командой дизайнеров, формирование языка дизайна, стыковка с командой бэкенда, унификация структуры данных и библиотеки компонентов также могут быть названы единственным способом для фронтенд-инженеров расширить поле своей работы.
Не бойтесь повторять колесо, ключ в том, чему вы учитесь на каждом колесе.
Поделиться с тобой.