Обзор

Застревание на react-router v3 затруднило построение поверх существующей инфраструктуры маршрутизации, так как это создало бы дополнительный технический долг, если бы его не обновили. По этой причине, хотя скоро будет выпущена v6, мы в QuickFrame сочли выгодным сначала обновиться до v5. Это позволило бы нам подготовить надлежащую декларативную маршрутизацию с <Switch> версии 5, тем самым облегчив обновление в будущем. (Здесь находится документ по миграции react-router с v5 на v6)

В этой статье я расскажу о том, как я перенес наши текущие маршруты со стандартов v3 на v5. Я предполагаю, что у вас уже достаточно знаний о реакции, а также о реакции-маршрутизаторе.

Вот основные шаги, которые я предпринял:

  • Обновление и установка пакетов npm
  • Реализация разделения кода
  • Настройка родительских маршрутов
  • Настройка подмаршрутов
  • Добавление авторизации

Процесс

Для начала я обновил все необходимые пакеты. В том числе react-router, react-router-dom, history. В настоящее время мы используем react-router-redux, поэтому мы также должны заменить этот устаревший пакет на connected-react-router, чтобы обработать нашу отправку и замену маршрута.

Так устроены наши маршруты. Довольно стандартная маршрутизация v3 с центральным компонентом <Route>.

<Route path={"/"} component={Layout}>
  <IndexRoute component={Home}/>
  <Route 
    path="/potato(/:subpage)"
    getComponent={Potato}
  />
  {/* other routes */}
  <Route path="*" getComponent={NotFound} />
</Route>

Во-первых, я просто обновил корневую оболочку для маршрутов, включив в нее <ConnectedRouter> из connected-react-router, и передал требуемый объект истории, созданный с использованием createBrowserHistory() из history (раньше он исходил из browserHistory, импортированного из react-router),

<Provider store={store}>
  <ConnectedRouter history={history}>
    <Routes />
  </ConnectedRouter>
</Provider>

Еще одна быстрая вещь, с которой нужно начать, — замена всех импортируемых <Link> с react-router на react-router-dom вместо этого.

Разделение кода

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

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

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

class DynamicImport extends React.Component {
  _isMounted = false
  state = {
    component: null,
  }
  componentDidMount() {
    const { load } = this.props
    this._isMounted = true
    load().then((component) => {
      if (this._isMounted) {
        this.setState(() => ({
          component: component.default 
            ? component.default 
            : component,
        }))
      }
    })
  }
  componentWillUnmount() {
    this._isMounted = false
  }
  render() {
    const { subpage = false, ...rest } = this.props
    const { component: Component } = this.state
    return !!Component ? (
      !subpage ? (
        <Layout>
          <Component {...rest} />
        </Layout>
      ) : (
        <Component {...rest} />
        )
      ) : (
        <Loading/>
      )
    }
  }
}

Настройка начальных маршрутов

Теперь с моим новым <DynamicImport> я могу продолжить реализацию наших новых маршрутов. Для меня было проще всего создать массив маршрутов для хранения всех путей и информации о компонентах для каждого маршрута и отобразить его в список <Routes>

После завершения это выглядело примерно так,

// we declare our paths and corresponding components in this array
const routes = [
  {
    path: '/potato',
    component: 'Potato',
  },
  //...rest of routes
]

Наши маршруты будут упакованы в пользовательскую оболочку, которая использует только что созданный <DynamicImport>.

Я называю эту пользовательскую оболочку <RouteWithSubRoutes>, поскольку переданный нами компонент Potato — это просто еще один <Route>, отображающий свои дочерние маршруты — предпочтительный способ обработки вложенных маршрутов в v4 или v5.

const RouteWithSubRoutes = ({ path, component }) => {
  const body = () => (
    <DynamicImport
      load={() => import(`./containers/${component}`)}
    />
  )
  return <Route path={path} component={body} />
}

Теперь мы можем создавать экземпляры наших вновь созданных маршрутов, используя компонент <Switch>, представленный в v4. Маршруты, вложенные в этот компонент, будут отображаться включительно — просто это означает, что будет отображаться только первый маршрут, соответствующий текущему URL-адресу.

Мы реализовали это так,

const routes = () => (
  <Switch>
    {routes.map(({ path, component }, i) => (
      <RouteWithSubRoutes 
        key={i} 
        path={path} 
        component={component} 
      />
    ))}
  </Switch>
)
export default routes

Настройка подмаршрутов

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

Взглянув на файл индекса для нашего компонента Potato, мы использовали специальный метод require.ensure для веб-пакета, который заменен простым imports.

Первоначальное намерение этого кода заключалось в том, чтобы иметь возможность действовать как способ декларативной маршрутизации, вызывая переключатель в параметре пути URL, объявленном с использованием "/potato(/:subpage)".

В этом конкретном случае мы отображали одну или две подстраницы,

if (typeof require.ensure !== 'function') {
  require.ensure = (d, c) => c(require)
}
export default function(location, cb) {
  switch (location.params.subpage) {
    Potato
      require.ensure([], (require) => {
       cb(null, require('./Subpage1').default)
      })
    break
   case 'subpage1':
      require.ensure([], (require) => {
        cb(null, require('./Subpage2').default
      })
    break
  }
}

Это определенно требует обновления.

Чтобы перенести этот код, я просто использовал новый <Switch> версии 5 с маршрутами, обернутыми нашим <DynamicImport> — аналогично нашим родительским маршрутам, но на этот раз с subpage = true.

Я закончил с этим обновлением,

/*
Our subPage helper would expect the following folder structure:
app
*
└─ Containers
   └─ Potato
      ├─ views
      │  ├─ Subpage1
      │  │  └─ index.js
      │  └─ Subpage2
      │     └─ index.js
      └─ index.js (current file)
*/
const subPage = ({ page, view }) => (props) => (
  <DynamicImport
    subpage
    load={() => import(`Containers/${page}/views/${view}`)}
    {...props}
  />
)
// Containers/Potato/index.js
const page = 'Potato'
const Subpage1 = subPage({ page, view: 'Subpage1' })
const Subpage2 = subPage({ page, view: 'Subpage2' })
const Potato = () => (
  <Switch>
    <Route path="/potato/subpage1" exact component={Subpage1} />
    <Route path="/potato/subpage2" exact component={Subpage2} />
  </Switch>
)
export default Potato

Здесь я просто определяю Potato как набор из двух подмаршрутов — по одному для обеих подстраниц на /subpage1 и /subpage2. Как только я убедился, что маршруты действительно работают, я экспортировал свой хелпер subPage и начал переносить остальные подмаршруты в приведенную выше структуру.

Повторное добавление авторизации

После переноса остальных файлов index.js для наших контейнеров мне пришлось добавить обратно нашу логику авторизации. Это было сделано для таких маршрутов, как /profile, и было реализовано с помощью v3s onEnter prop, который устарел, начиная с v4.

Для этого я начал с создания нового компонента-оболочки с именем <PrivateRoute>. Идея состоит в том, чтобы каждый маршрут, который я создал ранее в моем <RouteWithSubroutes>, был обернут этим новым компонентом, если путь требует авторизации. Это было достигнуто с помощью v4, встроенного в <Redirect>,

/*
  A Private Route requires a user 
  to be currently logged in to access
  - If there is no active user,
    they are redirected to the login page
 */
const PrivateRoute = ({ 
  routeToGo: Route,
  loggedIn, 
  ...rest 
}) => {
   const [isAuthed, setIsAuthed] = useState(true)
   useEffect(() => {
     if (!loggedIn) {
      setIsAuthed(false)
     }
   }, [loggedIn])
   return (
     <Route
       {...rest}
       render={(props) => 
         isAuthed
           ? Route
           : <Redirect to="/login" />}
    />
  )
}

С этим новым компонентом я сначала добавил новое поле с именем requiresValidLogin в исходный массив пар ключ/значение path и component для моих маршрутов.

const routes = [
  {
    path: '/profile',
    component: 'profile',
    requiresValidLogin: true
  },
  //...rest
]

Затем это поле передается моему начальному компоненту <RouteWithSubRoutes>, где я добавил эту дополнительную логику,

export const RouteWithSubRoutes = ({
  loggedIn, //login status
  path,
  component,
  requiresValidLogin, 
}) => {
  const body = (props) => (
    <DynamicImport
      load={() => import(`../containers/${component}`)}
      match={props.match}
    />
  )
  let routeToGo = <Route path={path} component={body} />
/*
 New logic to handle requiresValidLogin:
 */
 if (requiresValidLogin) {
    routeToGo = (
      <PrivateRoute routeToGo={routeToGo} loggedIn={loggedIn} />
    )
  } else if (loggedIn) {
    routeToGo = (
      <Redirect
        to={{
          pathname: '/',
        }}
      />
    )
  }
  return routeToGo
}

С этим новым выражением if проверяется requiresValidLogin, и если оно истинно, маршрут будет проходить через <PrivateRoute>. В противном случае, если false, пользователь будет перенаправлен обратно на целевую страницу, поскольку мы не хотим, чтобы авторизованный пользователь мог получить доступ к такой странице, как /login.

Последние штрихи

  • Одним из последних шагов, которые мне предстояло сделать, было приспособиться к изменениям в версии 4 и доступу к параметрам URL. Я больше не могу получить доступ к product_id таким образом, this.props.params.product_id. Вместо этого к нему нужно было получить доступ через свойство match react-router, например, this.props.match.params.product_id
  • Наконец, чтобы заменить наше использование react-router-redux, мне пришлось вместо этого импортировать push или replace из connected-react-router.
  • Были дополнительные случаи авторизации, которые мне нужно было учитывать, например, маршруты, к которым могли получить доступ только избранные персонажи (бренд или создатель в нашем случае). Для этого мне пришлось расширить свой <PrivateRoute> дополнительными помощниками и учесть дополнительные поля, переданные в мой массив маршрутов.

Заворачивать

Это обновление, безусловно, было трудоемким процессом, и, поскольку наша первоначальная маршрутизация была беспорядочной, пришлось также провести много рефакторинга. В целом, конечный продукт привел к более чистой кодовой базе, а также к правильной инфраструктуре маршрутизации. Это должно сократить объем рутинной работы, когда придет время перейти на v6.

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

Приложение живет здесь, если вы хотите его проверить! :)