Vue Router — посібник, якого бракувало

11 хв. читання
01 листопада 2018

Окрім маніпуляцій з DOM, обробки подій, форм та компонентів, кожен SPA фреймворк повинен мати дві основні функції, якщо він націлений на використання у великомасштабних застосунках:

  • Роутинг на стороні клієнта.
  • Явне управління станом (часто односпрямоване).

Vue пропонує офіційне рішення для роутингу та управління станом. У статті ми познайомимось зі vue-router, його поведінкою за різних сценаріїв та розглянемо декілька шаблонів для написання більш елегантного коду. Передбачається, що у вас вже є навички роботи з Vue, vue-router, та SPA.

Ми використаємо для прикладу застосунок з вбудованим режимом роутингу HTML5.

Маршрути

  • Список усіх користувачів у проекті /projects/:projectId/users
  • Вигляд інформації про окремого користувача /projects/:projectId/users/:userId
  • Вигляд профілю окремого користувача /projects/:projectId/users/:userId/profile
  • Створення нового користувача /projects/:projectId/users/new

Дерево компонентів:

Vue Router — посібник, якого бракувало
Ієрархія компонентів застосунку (походять від маршрутів)

1. Поточний об'єкт маршруту загальний та незмінний

Vue-router додає поточний маршрут у кожен компонент. Для цього треба вказати this.$route всередині такого компоненту. У цього об'єкта є дві особливості.

Об'єкти маршрутів незмінні

Якщо перейти до будь-якого маршруту з $router.push(), $router.replace() або за посиланням, створюється нова копія об'єкту $route. Вже наявний об'єкт не змінюється, тому не треба глибоко стежити за ним:

Vue.component('app-component', {
    watch: {
        $route: {
             handler() {},
             deep: true // <-- Не потрібно
        }
    }
});

Об'єкт маршруту доступний

Незмінність має наступні переваги. Роутер поділяє з усіма компонентами той самий екземпляр об'єкта $route. Такий код працюватиме:

// Батьківський компонент
Vue.component('app-component', {
    mounted() { window.obj1 = this.$route; }
});
// Дочірній компонент
Vue.component('user-list', {
    mounted() { window.obj2 = this.$route; }
});
// Після ініціалізації застосунку
window.obj1 === window.obj2; // <-- Це true

2. Vue-router — не роутер стану

Роутинг – перший рівень абстракції для декомпозиції великих веб-застосунків. Наступним рівнем є управління станом.

Є два способи організації декомпозиції вашого веб-застосунку. Або ви ділите застосунок на серії сторінок (тобто кожна сторінка визначається межами URL), або ваш застосунок являє собою набір чітко визначених станів (стан може не мати URL).

Роутер стану розкладає застосунок на набір станів. З url-роутером — на набір сторінок.

Vue роутер є url-роутером. У Vue немає офіційного роутера стану. Знайомі з Angular розробники одразу помітять різницю. Роутер стану відрізняється від url-роутера такими особливостями:

  • Роутер стану працює як кінцевий автомат.
  • URL не обов'язкові для роутера стану.
  • Стани можуть вкладатися.
  • Застосунок розбивається не на сторінки, а на добре визначений набір станів. Перехід від одного стану до іншого не завжди змінює URL.
  • При переході з одного стану в інший можуть передаватися будь-які складні дані. З URL-роутером дані, що передаються між сторінками, стають частиною URL-шляху або параметрами запиту.
  • З роутером стану передані дані втрачаються при повному перезавантаженні сторінки (якщо лише ви не використовуєте сесію чи локальне сховище). З URL-роутером можна відновити стан, тому що більшість переданих даних є в URL.

3. Неявна передача даних між маршрутами

Під час переходу від одного маршруту до іншого можна передавати комплексні дані, навіть якщо не використовувати роутер стану. І для цього не треба додавати нічого в URL.

Ви можете передати приховані дані/стан, коли переходите від одного маршруту до іншого з vue-router.

Де це може знадобитися? Зазвичай, при оптимізації. Розгляньмо наступний випадок:

  • У нас є дві сторінки:

    • Деталі —  /users/:userId
    • Профіль — /users/:userId/profile
  • На сторінці деталей ми здійснюємо один виклик API для отримання інформації про користувача. Також посилання на цій сторінці допомагає користувачеві перейти на сторінку профілю.

  • На другій сторінці необхідно здійснити два виклики API — отримати інформацію користувача та його стрічку.

  • Тут виникає проблема: необхідні два однакових виклики API при переході від деталей до сторінки профілю. Оптимальне рішення: коли користувач буде робити перехід, передавати вже отримані дані наступному маршруту. Ці дані не повинні бути частиною URL (те ж саме з роутером стану, де передається прихований стан).

  • Якщо користувач прямо переходить на сторінку профілю будь-яким іншим способом (наприклад, повністю перезавантажує сторінку), можна додатково перевірити доступність даних у хуку created.

// Всередині компоненту деталей користувача
Vue.component('user-details', {
    methods: {
        onLinkClick() {
            this.$router.push({ 
                name: 'profile',
                params: { 
                    userId: 123,
                    userData  // Приховані дані/стан
                }
            });
        }
    }
});
// Всередині компоненту профілю користувача
Vue.component('user-profile', {
    created() {

        if (this.$route.params.userData) {
            this.userData = this.$route.params.userData;
        } else {
            // Здійснюємо виклик API для отримання userData
            this.getUserDetails(this.$route.params.userId)
                .then(/* обробка відповіді */);
        }
    }
});

Зазначте: ми можемо так зробити, тому що об'єкт $route у кожному компоненті загальний та незмінний. Інакше усе було б важче.

4. Навігаційні хуки блокують батьківський компонент

Хук будь-якого дочірнього компонента може заблокувати батьківський компонент від відображення, якщо у вас вкладена конфігурація. Наприклад:

const ParentComp = Vue.extend({ 
    template: `<div>
        <progress-loader></progress-loader>
        <router-view>
    </div>` 
});
{
    path: '/projects/:projectId',
    name: 'project',
    component: ParentComp,
    children: [{
        path: 'users',
        name: 'list',
        component: UserList,
        beforeEnter (to, from, next) {
            setTimeout(() => next(), 2000);
        }
    }]
}

Якщо ви прямо переходите до /projects/100/users/list, то через асинхронний хук beforeEnter перенаправлення буде знаходитись в очікуванні й ParentComp не буде відображатися. Тому не очікуйте побачити progress-loader, поки хук не буде resolved. Те ж стосується будь-якого виклику API, який здійснюється батьківським компонентом.

Якщо ви хочете відобразити Батьківський компонент попри дозвіл хука, треба змінити ієрархію компонентів та оновити логіку progress-loader. Якщо це не варіант, зробіть подвійний перехідспочатку перейдіть до батьківського компонента, а потім до дочірнього:

goToUserList () {
    this.$router.push('/projects/100',
        () => this.$router.replace('users'))
}

Логіка тут є. Якщо батьківське представлення не очікує дочірнього хука, може статися так, що батьківське представлення з'явиться на мить, а потім, у разі збою хука, перейде в інше місце.

Зазначте: Роутинг в Angular працює інакше. Батьківський компонент, як правило, не очікує активації дочірніх хуків. Тож який підхід кращий? Ніякий. На перший погляд, підхід Angular здається більш природним та впорядкованим, але так можна легко зіпсувати UX, якщо розробник буде необережним.

У Vue роутері ієрархія маршрутів здається трохи незручною. Але так вірогідність пошкодити UX зменшується. Не забувайте також про область видимості у vue-router. У вас можуть бути global, route-level чи in-component хуки. Так можна отримати дуже надійний контроль.

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

5. Vue-router — не роутер на основі префіксного дерева

Vue роутер працює на основі path-to-regexp. Express.js роутинг використовує цю ж бібліотеку. Зіставлення URL засноване на регулярних виразах. Тобто ви можете визначити ваші маршрути так:

const prefix = `/projects/:projectId/users`;
const routes = [
    {
        path: `${prefix}/list`,
        name: 'user-list',
        component: UserList,
    },
    {
        path: `${prefix}/:userId`,
        name: 'user-details',
        component: UserDetails
    },
    {
        // ХІБА ЦЕ НЕ ПРОБЛЕМАТИЧНО?
        path: `${prefix}/new`,
        name: 'user-new',
        component: NewUser
    }
];

Зверніть увагу: для шляху ${prefix}/new ніколи не буде знайдено відповідність, тому що він визначений пізніше у маршруті. Це недолік роутингу на основі регулярних виразів. Кілька маршрутів можуть підходити одночасно. Звичайно, це не проблема для невеликих веб-застосунків. Розв'язати проблему можна визначивши маршрути як дерево:

const routes = [{
    path: '/projects/:projectId/users',
    name: 'project',
    component: ProjectUserView,
    children: [
        {
            path: '',
            name: 'list',
            component: UserList,
        },
        {
            path: 'new',
            name: 'user-details',
            component: NewUser,
        },
        {
            path: ':userId',
            name: 'user-new',
            component: UserDetails,
        }
    ]
}];

Є декілька переваг деревоподібної конфігурації:

  1. Вона очевидна і легко підтримується.
  2. Авторизацією/Хуками легко керувати. Правила GRUD стають дуже простими у реалізації.
  3. Більш передбачуваний роутинг у порівнянні з плоским списком маршруту.

Реалізуємо таку конфігурацію за допомогою проміжних компонентів, де є лише router-view компонент. Vue-router не дає кінцевим розробникам безпосередній доступ до компонента RouterView. Але невеликий трюк з огортання router-view допоможе значно зменшити кількість проміжних компонентів:

const RouterViewWrapper = Vue.extend({ 
    template: `<router-view></router-view>`
});
// Тепер, використовуйте компонент RouterViewWrapper, де необхідна
// деревоподібна конфігурація маршруту

Помітьте: Префіксне дерево (trie) є одним з видів дерева пошуку. Роутинг, що базується на префіксному дереві, є передбачуваним і не залежить від порядку визначення маршрутів. В екосистемі Node існує багато подібних роутерів. Hapi.js та Fastify.js застосовують цей вид роутингу.

Коротко кажучи:

Надавайте перевагу деревоподібній конфігурації перед плоскою.

6. Додаємо залежності в маршрут

У навігаційних хуках вам можуть знадобитися деякі залежності. Найбільш поширеним прикладом є сховище Vuex/Redux. Рішення потребує більше часу на організацію коду, ніж на сам роутер. Припустимо, у вас є наступні файли:

src/
  |-- main.js
  |-- router.js
  |-- store.js

Створимо функцію storeInjector, яку можна використовувати при оголошенні навігаційних хуків:

// У вашому store.js оголосіть storeInjector
export const store = new Vuex.Store({ /* config */ });
export function storeInjector(fn) {
    return (...args) => fn(...args, store);
}
// У вашому router.js використайте storeInjector
const routeConfig = {
    // Інші поля
    beforeEnter: storeInjector((to, from, next, store) => {})
}

Або огортаємо створення маршруту у функцію, куди можуть передаватися будь-які залежності.

// main.js файл
import { makeStore } from './store.js';
const store = makeStore();
const router = makeRouter(store);
const app = new Vue({ store, router, template: `<div></div>` });

// router.js файл
export function makeRouter(store) {
    // Якась робота зі сховищем
    return new VueRouter({
        routes: []
    })
}

7. Переглядаємо об'єкт маршруту один раз

Уявіть, що у вас є конфігурація маршруту з асинхронним компонентом. Асинхронні компоненти потрібні для відкладеного завантаження. Воно досягається розділенням пакетів такими засобами як Webpack або Rollup.

Так виглядатиме конфігурація:

const routes = [{
    path: '/projects/:projectId/users',
    name: 'user-list',
    // Асинхронний компонент (Розбиття коду з Webpack)
    component: import('../UserList.js'),
}];

В кореневому екземплярі або батьківському AppComponent може знадобитися projectId для здійснення деяких початкових викликів API. Типовий код:

Vue.component('app-comp', {
    created() {
        // Проблема: projectId є undefined         
        console.log(this.$route.params.projectId);
    }
	}

Тут виникає проблема. projectId невизначений, тому що дочірній компонент ще неготовий, а роутер не завершив перехід.

З асинхронними компонентами шлях або параметри запиту не будуть доступними всередині батьківського компонента, доки не буде створено дочірній компонент.

Тому будемо дивитися $route у батьківському компоненті. Треба дивитися його лише один раз, тому що це виклик API початкового завантаження, який не повинен виконуватись ще раз:

Vue.component('app-comp', {
    created() {
        const unwatch = this.$watch('$route', () => {
            const projectId = this.$route.params.projectId;
            
            // Решта роботи
            this.getProjectInfo(projectId);
            // Одразу викликаємо
            unwatch();
        });
    }
}

8. Комбінуємо вкладені компоненти з плоскими маршрутами

const routes = [{
    path: '/projects/:projectId',
    name: 'project',
    component: ProjectView,
    beforeEnter(to, from, next) {
        next();
    },
    children: [{
        // ОГЛЯДАЙТЕ ОБЕРЕЖНО
        // Вкладені маршрути починаються з `/`
        path: '/users',
        name: 'list',
        component: UserList,
    }]
}];

У наведеній конфігурації дочірній маршрут починається з / і розцінюється як кореневий шлях. Тому замість https://example.com/projects/100/users, можна використовувати https://example.com/users, щоб отримати доступ до компонента UserList. Однак він все одно відображатиметься як дочірній компонент ProjectView. Такі шляхи відомі як кореневі вкладені шляхи.

Звичайно, ієрархія компонентів. Навігаційні хуки все ще обробляються. <router-view> компоненти досі необхідні вам. Змінюється лише структура URL, тож хук beforeEnter виконається перед компонентом UserList.

Такий спосіб є зручним, однак, використовуйте його з розумом. Він має тенденцію заплутувати код. Кореневі вкладені шляхи є надзвичайно корисними у створенні PWA, де дуже помітна модель App Shell.

Офіційна організація роутингу у Vue є дуже гнучкою. Окрім простого роутингу, існує багато фіч: meta-поля, transitions, просунутий scroll-behavior, lazy-loading тощо.

Також Vue роутер розроблено з урахуванням UX міркувань: навігаційні хуки, попередня вибірка даних. Ви можете використовувати глобальні або вбудовані в компоненти хуки, але використовуйте їх з розумом. Слід пам'ятати про поділ відповідальності та визначати обов'язки роутингу поза компонентами.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 869
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід