Вы когда-нибудь думали, что React или Vue могут устареть и остаться на обочине жизни вместе с другими фреймворками и библиотеками (такими как jQuery, Backbone и т. д.)?

В это трудно поверить.

Мы в goLance тоже об этом не подумали, когда начинали проект на Angular 1 в далеком 2015 году.

Но вот наступил 2019 год.

Сообщество фронтенда пишет на React, Vue 2 и Angular 4.

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

  • Больше не поддерживается авторами и сообществом
  • Сложно найти новых сотрудников
  • Мы используем gulp и grunt для сборки, и эти инструменты устарели.
  • Медленная скорость разработки по сравнению с фреймворками, которые были на рынке.

Мы решили использовать Vue 2 для реализации нового дизайна. Клиент поддержал предложение по замене техники, но поставил ряд условий:

  • Старый дизайн остался прежним
  • Новые страницы будут открываться страница за страницей
  • Старые страницы могут вести к новым и наоборот

Выполнение

Особенность современных фреймворков в том, что их можно внедрить в любой проект, с любыми существующими технологиями. Когда Facebook переписывал свое приложение на React, часть страниц работала на React, а остальные — нет.

Было несложно запустить новое приложение Vue вместе со старым. Самым важным был переход на новые страницы и переходы между старыми и новостными. Мы сосредоточились на его проблеме.

Все приложение было основано на библиотеке Angular UI Router, которая выполняет маршрутизацию.

Вот так выглядит обычный маршрут (состояние).

$stateProvider
  .state('account', {
    url: '/account',
    templateUrl: '/client/partial/account/account.html',
    controller: 'AccountCtrl',
});

Angular 1 следует шаблону MVC, поэтому templateUrl — это представление, а controller — класс контроллера.

Для переключения страниц на Vue 2 мы написали простенькую функцию-обертку:

const createVueRedirectState = ({
  name, 
  url,
}) => {
  const controller = ($state, $stateParams, $location) => {
    // $state is object describes route
    if (!window.$state) {
      window.$state = $state;
    }

    // Store in Vuex state with key 'router' 
    window.$store.commit('setRouterState', {
      name,
      params: $state.params,
      query: $location.search(),
      isVueRoute: true,
    });
  };

  const template = '<div></div>'; // Renders empty template

  return {
    url,
    name,
    template,
    controller,
  };
};

Оболочка берет имя маршрута (необходимое для маршрутизатора пользовательского интерфейса) и URL-адрес и создает шаблон с пустым div и контроллером, который просто записывает в Vuex Store (window.$store) данные о состоянии маршрутизации.

Теперь вы можете заменить старое состояние на новое:

$stateProvider
 .state('account', createVueRedirectState({
    name: 'account',
    url: '/account',
  }))

Маршрутизатор пользовательского интерфейса отображает страницу внутри тега с атрибутом ui-view в файле index.html.

Мы решили использовать это для рендеринга старых страниц внутри Vue.js.

Шаблон корневого файла App.vue выглядит следующим образом:

<template>
  <div>
    <main>
      <div
        v-show="!isVueRoute"
        class="old-app"
      >
        <!-- Angular components -->
        <div ui-view></div>
        <!-- END: Angular components -->
      </div>

      <!-- Vue application -->
      <div class="new-app" v-if="isVueRoute">
        <router-view/>
      </div>
    </main>
</template>

<script>
  import { mapGetters } from 'vuex';
  import Vue from 'vue';

  import RouterView from '@/components/router/RouterView.vue';

  export default Vue.extend({
    components: {
      RouterView,
    },

    computed: {
      ...mapGetters(['router']),

      isVueRoute() {
        return this.router.isVueRoute;
      },
    }
  });
</script>

Как было сказано выше, UI Router отвечает за управление состоянием маршрутизации приложения, что не позволяет нам использовать библиотеку Vue Router. Поэтому мы решили максимально близко сымитировать поведение ее основных компонентов router-view и router-link для безболезненного перехода на использование этой библиотеки в будущем.

Прежде всего, маршруты, совместимые с Vue Router, были описаны следующим образом:

// router/routes.js

export default const routes = [
  {
    path: '/:contractId?',
    name: 'Main',
    component: Main,
    meta: {
      requiresAuth: true,
      layout: MainLayout,
    },
  },
  {
    path: '/sign-in',
    name: 'SignIn',
    component: () => import(/* webpackChunkName: "sign-in" */ '../views/sign-in/SignIn.vue'),
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/about/About.vue'),

    meta: {
      requiredAuth: true,
      layout: NavigationLayout,
    },
  },
 ...
];

Далее реализуем router-view в Vue:

<template>
  <component :is="pageToRender" /> 
</template>

<script>
  import Vue from 'vue';
  import _find from 'lodash/find';
  import routes from 'router/routes';

  export default Vue.extend({
    computed: {
      pageToRender() {
        if (this.$store.state.$router.state) {
          const currentAngularState = this.$store.state;
          const route = routes.find(r => r.name = currentAngularState.$router.state.name);
          return router.component; // For example SignIn component above
        }

        return null;
      },
    },
  });
</script>

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

Теперь роутер-ссылка:

<template>
  <a :href="href">
    <slot></slot>
  </a>
</template>

<script>
  import Vue from 'vue';

  export default Vue.extend({
    props: {
      to: {
        type: Object,
        default() {
          return {};
        },
      },
    },
    computed: {
      href() {
        return window.$state.href(this.to.name, this.to.params);
      },
    },
  });
</script>

Где window.$state — глобально доступное состояние, а href — состояние маршрутизатора библиотеки пользовательского интерфейса, который создает URL-адрес на основе переданных параметров.

Пример соединения с маршрутизатором:

<router-link :to="{ name: 'account' }">
  <user-avatar
    :url="user.url"
    class="user-avatar"
    size="64"
  />
</router-link>

Как это работает

Давайте посмотрим, что мы сделали в целом. В качестве примера я возьму маршрут /account:

  1. <router-link :to=”{ name: ‘account’ }”> создает ссылку /account
  2. Когда пользователи нажимают на эту ссылку, они перенаправляются на /account
  3. Перенаправление перехватывается $stateProvider, который сохраняет маршрут в хранилище Vuex.
  4. Затем повторно отображается App.vue (см. код выше).
    Он берет маршрут из хранилища Vuex, и если это новый маршрут (флаг isVueRoute), то компонент страницы Vue и пустой угловой компонент ( помните <div></div> ?) рендерятся.
    В противном случае App.vue рендерит <div ui-view></div> и внутри рендерится старая угловая страница.

Полученные результаты

Итак, чего мы достигли?

Самый большой плюс — плавный переход на Vue 2, который не блокирует разработку новых страниц приложения и обслуживание старых.

Недостатков у нас больше:

  • Большой размер пакета, потому что теперь у нас есть 2 фреймворка.
  • Запуск и просмотр проекта, время строительства увеличено
  • Время загрузки страниц увеличено
  • Трудно одновременно поддерживать 2 фреймворка с точки зрения разработчиков
  • Иногда приходится прибегать к дублированию кода из-за разных модульных фреймворков (AMD — AngularJS, ESModules — Vue.js).

Что дальше?

На данный момент заменены почти все страницы приложения. Теперь мы планируем избавиться от UI-Router и использовать Vue Router. Недалек тот час, когда у нас не останется ни строчки кода AngularJS!

Статья написана в соавторстве с Сергеем Батурой.