Исследование react-router + react-transition-group для реализации анимации перехода

React.js

исходный адрес

1. Introduction

В повседневной разработке анимация перехода при переключении страниц является относительно базовой сценой. В реактивных проектах мы обычно используемreact-routerдля управления роутингом, но react-router не предоставляет соответствующей функции анимации перехода, а напрямую подменяет компонент очень тупо. В какой-то степени опыт был не таким уж дружелюбным.

Чтобы добиться эффектов анимации в реакции, у нас есть много вариантов, таких как:react-transition-group,react-motion,Animatedи т.п. Однако из-за набора хуков enter, enter-active, exit, exit-active, добавленных к элементу группой react-transition-group, он просто предназначен для входа и выхода с нашей страницы. Исходя из этого, в этой статье для достижения анимационных эффектов выбрана группа react-transition.

Далее в этой статье они будут объединены, чтобы дать представление о реализации анимации перехода маршрутизации.

2. Requirements

Давайте сначала определим, какой эффект будет выполнять анимация перехода. Как показано ниже:

3. react-router

Во-первых, давайте кратко представим основы использования react-router (см.Введение на официальном сайте).

Здесь мы будем использовать те, которые предоставляет react-router.BrowserRouter,Switch,Routeтри компонента.

  • BrowserRouter: Маршрутизация реализована в виде истории апи, предоставляемой html5 (также есть маршрутизация, реализованная в виде хэша).
  • Switch: Когда несколько компонентов маршрута сопоставляются одновременно, они будут отображаться по умолчанию, но компонент маршрута, обернутый коммутатором, будет отображать только первый совпадающий маршрут.
  • Route: Компонент маршрута, путь указывает соответствующий маршрут, а компонент указывает компонент, отображаемый при совпадении маршрута.
// src/App1/index.js
export default class App1 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Switch>
          <Route exact path={'/'} component={HomePage}/>
          <Route exact path={'/about'} component={AboutPage}/>
          <Route exact path={'/list'} component={ListPage}/>
          <Route exact path={'/detail'} component={DetailPage}/>
        </Switch>
      </BrowserRouter>
    );
  }
}

Как показано выше, это часть реализации ключа маршрутизации. Всего мы создали首页,关于页,列表页,详情页эти четыре страницы. Отношение скачка:

  1. Главная ↔ О странице
  2. Домашняя страница ↔ Страница со списком ↔ Страница с подробностями

Давайте посмотрим на текущий эффект переключения маршрутизации по умолчанию:

4. react-transition-group

Из приведенных выше изображений мы видим, что react-router вообще не имеет эффекта перехода при переключении маршрута, а напрямую заменяется, что очень грубо.

Как говорится, если вы хотите сделать хорошую работу, вы должны сначала отточить свои инструменты.Прежде чем внедрять реализацию анимации перехода, мы должны сначала научиться использовать react-transition-group. Исходя из этого, мы кратко представим компоненты CSSTransition и TransitionGroup, которые он предоставляет.

4.1 CSSTransition

CSSTransition — это компонент, предоставленный react-transition-group, вот краткое введение в принцип его работы.

When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick. CSSTransition forces a reflow between before adding the example-enter-active. This is an important trick because it allows us to transition between example-enter and example-enter-active even though they were added immediately one after another. Most notably, this is what makes it possible for us to animate appearance.

Это описание с официального сайта, которое означает, что когда для свойства in CSSTransition установлено значение true, CSSTransition сначала добавит класс xxx-enter к своим подкомпонентам, а затем сразу же добавит класс xxx-enter-active следующий тик-класс. Таким образом, мы можем воспользоваться этим и позволить элементу плавно переходить между двумя состояниями с помощью свойства перехода CSS, чтобы получить соответствующий эффект анимации.

И наоборот, когда для свойства in установлено значение false, CSSTransition добавит классы xxx-exit и xxx-exit-active к дочерним компонентам. (Для получения более подробной информации, пожалуйста, нажмитеОфициальный сайтПроверить)

Основываясь на двух вышеприведенных пунктах, нужно ли нам просто заранее написать стиль CSS, соответствующий классу? Вы можете попробовать небольшую демонстрацию, как показано в следующем коде:

// src/App2/index.js
export default class App2 extends React.PureComponent {

  state = {show: true};

  onToggle = () => this.setState({show: !this.state.show});

  render() {
    const {show} = this.state;
    return (
      <div className={'container'}>
        <div className={'square-wrapper'}>
          <CSSTransition
            in={show}
            timeout={500}
            classNames={'fade'}
            unmountOnExit={true}
          >
            <div className={'square'} />
          </CSSTransition>
        </div>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}
/* src/App2/index.css */
.fade-enter {
  opacity: 0;
  transform: translateX(100%);
}

.fade-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.fade-exit {
  opacity: 1;
  transform: translateX(0);
}

.fade-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

Давайте посмотрим на эффект, похож ли он на эффект входа и выхода со страницы?

4.2 TransitionGroup

Очень удобно использовать CSSTransition для управления анимацией, но анимация, непосредственно используемая для управления несколькими страницами, все еще немного тонкая. По этой причине давайте представим компонент TransitionGroup, предоставляемый react-transition-group.

The component manages a set of transition components ( and ) in a list. Like with the transition components, is a state machine for managing the mounting and unmounting of components over time.

Как представлено на официальном веб-сайте, компонент TransitionGroup — это компонент, используемый для управления процессом монтирования и размонтирования группы узлов, что очень подходит для работы с несколькими страницами здесь. Это введение кажется немного трудным для понимания, поэтому давайте посмотрим на фрагмент кода, чтобы объяснить, как работает TransitionGroup.

// src/App3/index.js
export default class App3 extends React.PureComponent {

  state = {num: 0};

  onToggle = () => this.setState({num: (this.state.num + 1) % 2});

  render() {
    const {num} = this.state;
    return (
      <div className={'container'}>
        <TransitionGroup className={'square-wrapper'}>
          <CSSTransition
            key={num}
            timeout={500}
            classNames={'fade'}
          >
            <div className={'square'}>{num}</div>
          </CSSTransition>
        </TransitionGroup>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}

Давайте сначала посмотрим на эффект, а затем объясним:

Сравнив код App3 и App2, мы можем обнаружить, что этот CSSTransition не имеет атрибута in, но использует атрибут key. Но почему он до сих пор работает нормально?

Прежде чем ответить на этот вопрос, давайте подумаем над вопросом:

Поскольку механизм dom diff реакции использует атрибут ключа, если два ключа до и после различаются, реакция выгрузит старый узел и смонтирует новый узел. Итак, в приведенном выше коде старый узел не должен немедленно исчезнуть, потому что ключ изменился, но почему мы все еще можем видеть процесс анимации его исчезновения?

Ключ находится в TransitionGroup, потому что, когда она заметит изменение своих дочерних элементов, она сначала сохранит узел, который собирается удалить, и фактически удалит узел, когда его анимация закончится.

Таким образом, в приведенном выше примере, когда мы нажимаем кнопку переключения, процесс изменения может быть понятен следующим образом:

<TransitionGroup>
  <div>0</div>
</TransitionGroup>

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

<TransitionGroup>
  <div>0</div>
  <div>1</div>
</TransitionGroup>

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

<TransitionGroup>
  <div>1</div>
</TransitionGroup>

Как объяснялось выше, мы можем тонко использовать изменение значения ключа, чтобы позволить TransitionGroup взять на себя создание и уничтожение страницы во время перехода, и нужно только обратить внимание на то, как выбрать соответствующее значение ключа и какой стиль CSS необходим для добиться эффекта анимации Вот и все.

5. Page transition animation

Основываясь на представлении react-router и react-transition-group выше, мы освоили основы, а затем можем объединить их, чтобы создать анимацию перехода для переключения страниц.

Как упоминалось в конце предыдущего раздела, после использования TransitionGroup нашей проблемой становится выбор подходящего значения ключа. Итак, в системе маршрутизации какое значение ключа является подходящим?

Поскольку мы запускаем анимацию перехода при переключении страницы, естественно, что значение, связанное с маршрутом, подходит в качестве значения ключа. И в реактивном маршрутизатореlocationУ объекта есть ключевое свойство, которое изменяется при изменении адреса в браузере. Однако в реальных сценариях это кажется неподходящим, поскольку изменение параметра запроса или хэша также приведет к изменению location.key, но часто в этих сценариях не требуется запускать анимацию перехода.

Поэтому лично мне кажется, что выбор ключевых значений все же зависит от разных проектов. В большинстве случаев рекомендуется использовать location.pathname в качестве значения ключа, потому что это именно маршрут наших разных страниц.

Сказав это, давайте посмотрим, как конкретный код применяет react-transition-group к react-router:

// src/App4/index.js
const Routes = withRouter(({location}) => (
  <TransitionGroup className={'router-wrapper'}>
    <CSSTransition
      timeout={5000}
      classNames={'fade'}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

export default class App4 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Routes/>
      </BrowserRouter>
    );
  }
}

Вот эффект:

Идея кода App4 примерно такая же, как у App3, за исключением того, что исходный div заменяется компонентом Switch, а также используется withRouter.

withRouter — это высокоуровневый компонент, предоставляемый react-router, который может предоставлять местоположение, историю и другие объекты для ваших компонентов. Поскольку мы используем location.pathname в качестве ключевого значения CSSTransition здесь, он используется.

Кроме того, здесь есть яма, которая является свойством локации Switch.

A location object to be used for matching children elements instead of the current history location (usually the current browser URL).

Это описание на официальном веб-сайте, что означает, что компонент Switch будет использовать этот объект для сопоставления маршрута в своих дочерних элементах, и по умолчанию используется URL-адрес текущего браузера. Если мы не укажем его в приведенном выше примере, то в анимации перехода произойдет очень странное явление, то есть два одинаковых узла движутся одновременно. . . Как следующее:

Это связано с тем, что компонент TransitionGroup сохранит удаляемый узел Switch, но при изменении местоположения старый узел Switch будет использовать измененное местоположение для сопоставления маршрута в своих дочерних элементах. Поскольку все местоположения обновлены, страницы, соответствующие двум переключателям, одинаковы. К счастью, мы можем изменить свойство местоположения коммутатора, как показано в приведенном выше коде, чтобы оно не всегда соответствовало текущему местоположению.

6. Page dynamic transition animation

Хотя в предыдущей статье для реализации простой анимации перехода использовалась группа react-transition-group и react-router, здесь есть серьезная проблема. Внимательно изучите схематическую диаграмму предыдущего раздела, нетрудно обнаружить, что наш анимационный эффект входа на следующую страницу такой, как и ожидалось, но какой, черт возьми, анимационный эффект возврата назад. . . Очевидно, что предыдущая страница должна появляться слева, а текущая страница должна исчезать справа. Но почему получается, что текущая страница затухает слева, а следующая загорается справа, что равносильно входу на следующую страницу. На самом деле причина ошибки очень проста:

Во-первых, мы разделяем изменения маршрутизации на две операции: вперед и назад. В операции «вперед» эффект выхода текущей страницы должен постепенно исчезать влево; в операции «назад» эффект выхода текущей страницы должен исчезать вправо. Поэтому мы используем только два класса, fade-exit и fade-exit-active Очевидно, что получаемый эффект анимации должен быть одинаковым.

Поэтому решение тоже очень простое: мы можем использовать два набора классов для управления анимационными эффектами операций вперед и назад соответственно.

/* src/App5/index.css */

/* 路由前进时的入场/离场动画 */
.forward-enter {
  opacity: 0;
  transform: translateX(100%);
}

.forward-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.forward-exit {
  opacity: 1;
  transform: translateX(0);
}

.forward-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

/* 路由后退时的入场/离场动画 */
.back-enter {
  opacity: 0;
  transform: translateX(-100%);
}

.back-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.back-exit {
  opacity: 1;
  transform: translateX(0);
}

.back-exit-active {
  opacity: 0;
  transform: translate(100%);
  transition: all 500ms;
}

Однако одной поддержки CSS недостаточно, необходимо добавлять соответствующие классы к различным операциям маршрутизации. Потом снова возникает проблема.Под руководством TransitionGroup, после монтирования компонента фактически определена его выходная анимация.Вы можете увидеть это на официальном сайте.issue. Другими словами, даже если мы динамически добавим различные свойства ClassNames в CSSTransition для указания эффектов анимации, на самом деле это недействительно.

Решение действительно естьissueКак показано ниже, мы можем использовать свойство ChildFactory TransitionGroup и метод React.cloneElement, чтобы принудительно переопределить его className. Например:

<TransitionGroup childFactory={child => React.cloneElement(child, {
  classNames: 'your-animation-class-name'
})}>
  <CSSTransition>
    ...
  </CSSTransition>
</TransitionGroup>

После того, как вышеуказанные проблемы решены, остается проблема выбора подходящего класса анимации. Суть этой проблемы заключается в том, как определить, является ли текущее изменение маршрута прямой или обратной операцией. К счастью, react-router тщательно подготовил его для нас.У объекта истории, который он предоставляет, есть атрибут действия, который представляет тип текущего изменения маршрута, и его значение равно'PUSH' | 'POP' | 'REPLACE'. Итак, давайте снова скорректируем код:

// src/App5/index.js
const ANIMATION_MAP = {
  PUSH: 'forward',
  POP: 'back'
}

const Routes = withRouter(({location, history}) => (
  <TransitionGroup
    className={'router-wrapper'}
    childFactory={child => React.cloneElement(
      child,
      {classNames: ANIMATION_MAP[history.action]}
    )}
  >
    <CSSTransition
      timeout={500}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

Давайте посмотрим на измененный эффект анимации:

7. Optimize

На самом деле содержимое этого раздела не оптимизировано.Идея анимации перехода в основном здесь закончилась.Вы можете открыть свой разум и добавить css для получения более крутой анимации перехода. Тем не менее, я все же хочу поговорить о том, как написать нашу маршрутизацию более настраиваемой (личное предпочтение, не распыляйтесь, если не нравится).

Мы знаем, что react-router сильно изменился при обновлении v4. Более уважаема динамическая маршрутизация, а не статическая маршрутизация. Тем не менее, конкретные проблемы анализируются подробно.В некоторых проектах отдельные лица по-прежнему любят централизовать управление маршрутами.Для приведенного выше примера я надеюсь иметь RouteConfig, как показано ниже:

// src/App6/RouteConfig.js
export const RouterConfig = [
  {
    path: '/',
    component: HomePage
  },
  {
    path: '/about',
    component: AboutPage,
    sceneConfig: {
      enter: 'from-bottom',
      exit: 'to-bottom'
    }
  },
  {
    path: '/list',
    component: ListPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  },
  {
    path: '/detail',
    component: DetailPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  }
];

С помощью приведенного выше RouterConfig мы можем четко знать, какой компонент соответствует каждой странице, а также мы можем знать, что такое эффект анимации перехода, например关于页面вводится снизу страницы,列表页а также详情页Все заходят на страницу справа. В общем, мы можем получить много полезной информации непосредственно через эту таблицу конфигурации статической маршрутизации, не углубляясь в код для получения информации.

Итак, как следует настроить соответствующий код маршрутизации для вышеуказанного требования? пожалуйста, посмотрите ниже:

// src/App6/index.js
const DEFAULT_SCENE_CONFIG = {
  enter: 'from-right',
  exit: 'to-exit'
};

const getSceneConfig = location => {
  const matchedRoute = RouterConfig.find(config => new RegExp(`^${config.path}$`).test(location.pathname));
  return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG;
};

let oldLocation = null;
const Routes = withRouter(({location, history}) => {

  // 转场动画应该都是采用当前页面的sceneConfig,所以:
  // push操作时,用新location匹配的路由sceneConfig
  // pop操作时,用旧location匹配的路由sceneConfig
  let classNames = '';
  if(history.action === 'PUSH') {
    classNames = 'forward-' + getSceneConfig(location).enter;
  } else if(history.action === 'POP' && oldLocation) {
    classNames = 'back-' + getSceneConfig(oldLocation).exit;
  }

  // 更新旧location
  oldLocation = location;

  return (
    <TransitionGroup
      className={'router-wrapper'}
      childFactory={child => React.cloneElement(child, {classNames})}
    >
      <CSSTransition timeout={500} key={location.pathname}>
        <Switch location={location}>
          {RouterConfig.map((config, index) => (
            <Route exact key={index} {...config}/>
          ))}
        </Switch>
      </CSSTransition>
    </TransitionGroup>
  );
});

Так как кода css много, я не буду его здесь выкладывать, но это не что иное, как соответствующая конфигурация анимации перехода, полный код можно увидетьрепозиторий на гитхабе. Давайте посмотрим на текущий эффект:

8. Summarize

В этой статье сначала кратко рассказывается об основных принципах использования react-router и react-transition-group, анализируется принцип работы CSSTransition и TransitionGroup для создания анимации, а затем объединяется react-router и react-transition-group Завершение перехода попытка анимации; и использовать свойство childFactory группы TransitionGroup для решения проблемы анимации динамического перехода; наконец, настроить маршрутизацию, реализовать унифицированное управление маршрутизацией и конфигурацией анимации, а также завершить реакцию-маршрутизатор + реакция-переход-группа, реализующая Изучение анимации перехода.

9. Reference

  1. A shallow dive into router v4 animated transitions
  2. Dynamic transitions with react router and react transition group
  3. Issue#182 of react-transition-group
  4. StackOverflow: react-transition-group and react clone element do not send updated props

Весь код в этой статье размещен по адресуздесь, если вы считаете, что это хорошо, вы можете датьstar.