Использование middleware во Vue

Spread the love

Как правило, при создании SPA необходимо защищать определенные маршруты. Например, допустим, у нас есть маршрут к информационной панели, к которому могут обращаться только аутентифицированные пользователи, мы можем использовать промежуточное программное обеспечение (middleware) для аутентификации, чтобы гарантировать, что только аутентифицированные пользователи имеют доступ к маршруту на информационной панели.

В этом руководстве мы покажем, как реализовать использование промежуточного программного обеспечения (middleware) для приложения Vue с использованием Vue-Router.

Что такое middleware pipeline?

Конвейер промежуточного программного обеспечения (middleware pipeline) – это стек различных промежуточных программ, запускаемых параллельно друг другу.

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

Начнем

Для начала мы будем использовать интерфейс командной строки Vue CLI для быстрого создания нового проекта Vue.

vue create vue-middleware-pipeline

Установка зависимостей

После того, как был создан каталог проекта, перейдите во внутрь созданного каталога и выполните следующую команду из терминала:

npm i vue-router vuex

Vue-router —  это официальный роутер для Vue.js

Vuex —  библиотека управления состоянием для Vue

Создание компонентов

Наше приложение будет состоять из трех компонентов.

Login — Этот компонент будет отображаться пользователям, которые не прошли проверку подлинности.

Dashboard — Этот компонент будет отображаться для пользователей, которые вошли в систему.

Movies — Мы покажем этот компонент пользователям, которые вошли в систему и имеют активную подписку.

Давайте создадим эти компоненты. Перейдите в каталог src/components и создайте следующие файлы: Dashboard.vue, Login.vue и Movies.vue

Отредактируйте файл Login.vue:

<template>
  <div>
    <p>This is the Login component</p>
  </div>
</template>

Отредактируйте файл Dashboard.vue:

<template>
  <div>
    <p>This is the Dashboard component for authenticated users</p>
    <router-view/>
  </div>
</template>

Наконец, добавьте следующий код в файл Movies.vue:

<template>
  <div>
    <p>This is the Movies component for authenticated and subscribed users</p>
  </div>
</template>

Создание хранилища store

В отношении Vuex store – это просто контейнер для хранения состояния нашего приложения. Оно позволит нам определить, аутентифицирован ли пользователь, а также проверить, подписан ли пользователь или нет.
Внутри папки src создайте файл store.js и добавьте в него следующий код:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)


export default new Vuex.Store({
    state: {
        user: {
            loggedIn: false,
            isSubscribed: false
        }
    },

    getters: {
        auth(state) {
            return state.user
        }
    }
})

Хранилище содержит объект пользователя user. Пользовательский объект содержит свойство loggedIn и isSubscribeed, которые помогают нам определить, вошел ли пользователь в систему и имеет ли активную подписку. Мы также определили геттер внутри store, который возвращает объект user.

Определение наших маршрутов

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

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

/dashboard/ будет доступна только для аутентифицированных пользователей. Неаутентифицированные пользователи должны быть перенаправлены на маршрут /login, когда они посещают этот маршрут. Мы свяжем аутентификационное промежуточное ПО с этим маршрутом.

/dashboard/movies будут доступны только для аутентифицированных и подписанных пользователей. Этот маршрут будет защищен промежуточным ПО isSubscribeed и auth.

Создание маршрутов

Затем создайте папку router в каталоге src, а затем создайте файл router.js внутри этой папки. В несите следующий код в этот файл:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [
        {
            path: '/login',
            name: 'login',
            component: Login
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies
            }
        ],
        }
    ]
})


export default router

Здесь мы создали новый экземпляр маршрутизатора, передавая пару параметров конфигурации, а также свойство routes, которое принимает все маршруты, которые мы определили ранее. На данный момент важно отметить, что ни один из этих маршрутов не защищен. Мы исправим это в ближайшее время.
Далее, давайте добавим router и наше хранилище store в экземпляр Vue. Отредактируйте файл src/main.js с кодом ниже:

import Vue from 'vue'
import App from './App.vue'
import router from './router/router'
import store from './store'

Vue.config.productionTip = false


new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

Создание промежуточного программного обеспечения

В каталоге src/router создайте папку middleware, а затем в этой папке создайте файлы guest.js, auth.js и IsSubscribeed.js. Добавьте следующий код в файл guest.js:

export default function guest ({ next, store }){
    if(store.getters.auth.loggedIn){
        return next({
           name: 'dashboard'
        })
    }
   
    return next()
   }

guest.js проверяет, прошел ли аутентификацию пользователь. Если он аутентифицирован, он перенаправляется на маршрут к dashboard.

Затем следующий код в файл auth.js :

export default function auth ({ next, store }){
 if(!store.getters.auth.loggedIn){
     return next({
        name: 'login'
     })
 }

 return next()
}

auth.js используя хранилище, проверяет, аутентифицирован ли пользователь в данный момент. В зависимости от того, вошел ли пользователь в систему или нет, мы либо продолжим выполнение запроса, либо перенаправим его на страницу входа.

Внесите следующий код в файл isSubscribeed.js:

export default function isSubscribed ({ next, store }){
    if(!store.getters.auth.isSubscribed){
        return next({
           name: 'dashboard'
        })
    }
   
    return next()
   }

isSubscribeed.js аналогичен auth.js. Используя store, в нем проверяется, подписан ли пользователь. Если пользователь подписан, он может получить доступ к намеченному маршруту или же перенаправлен обратно на страницу панели мониторинга.

Защита маршрутов

Теперь, когда мы создали все наши промежуточные программы, давайте использовать их для защиты наших маршрутов. Отредактируйте файл src/router/router.js с помощью следующего кода:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'

import guest from './middleware/guest'
import auth from './middleware/auth'
import isSubscribed from './middleware/isSubscribed'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [{
            path: '/login',
            name: 'login',
            component: Login,
            meta: {
                middleware: [
                    guest
                ]
            }
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
                middleware: [
                    auth
                ]
            },
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies,
                meta: {
                    middleware: [
                        auth,
                        isSubscribed
                    ]
                }
            }],
        }
    ]
})


export default router

Здесь мы импортировали все наше промежуточное программное обеспечение, а затем для каждого из маршрутов мы определили поле meta, содержащее массив middleware. Массив middleware содержит все промежуточное программное обеспечение, которое мы хотим связать с определенным маршрутом.

Vue Router навигационные guards

Навигационные средства защиты (navigation guards), предоставляемые Vue Router, являются единственной причиной, по которой мы можем использовать промежуточное ПО для защиты наших маршрутов. Эти средства навигации в основном используются для защиты маршрутов путем их перенаправления или отмены.

Один из этих guards – это глобальный before guard, который обычно представляет собой hook, вызываемый непосредственно перед срабатыванием маршрута. Чтобы зарегистрировать глобальный before guard, мы определяем метод beforeEach на экземпляре маршрутизатора.

const router = new Router({ ... })
router.beforeEach((to, from, next) => {
 //necessary logic to resolve the hook
})

Метод beforeEach получает три аргумента:

to: Это маршрут, к которому мы намерены получить доступ.

from: Это маршрут, по которому мы сейчас идем.

next: Это функция, вызываемая для разрешения хука.

Запуск middleware

Используя хук beforeEach, мы можем запустить наше промежуточное ПО.

const router = new Router({ ...})

router.beforeEach((to, from, next) => {
    if (!to.meta.middleware) {
        return next()
    }
    const middleware = to.meta.middleware

    const context = {
        to,
        from,
        next,
        store
    }
    return middleware[0]({
        ...context
    })
})

Сначала мы проверяем, есть ли у текущего обрабатываемого маршрута поле meta, содержащее свойство middleware. Если свойство middleware найдено, мы присваиваем его переменной const middleware. Далее мы определяем объект context, который содержит все, что нам нужно что бы передать каждому middleware. Затем мы вызываем самое первое middleware в массиве middleware как функцию и передаем объект context.

Попробуйте посетить маршрут /dashboard, вы должны быть перенаправлены на страницу login. Это связано с тем, что свойство store.state.user.loggedIn в нашем /src/store.js имеет значение false. Измените свойство store.state.user.loggedIn на true, и теперь вы сможете получить доступ к маршруту /dashboard.

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

return middleware[0]({ …context})

Обратите внимание на эту строку кода из приведенного выше блока кода, мы вызываем только первый кусок промежуточного программного обеспечения, переданный из массива middleware. Как тогда нам обеспечить, чтобы другие middleware, содержащиеся в массиве, тоже вызывались? Тут нам нужно конвейер (pipeline).

Создание конвейера

Перейдите в каталог src/router и затем создайте файл middlewarePipeline.js. Добавьте в него следующий код:

function middlewarePipeline (context, middleware, index) {
    const nextMiddleware = middleware[index]

    if(!nextMiddleware){
        return context.next 
    }

    return () => {
        const nextPipeline = middlewarePipeline(
            context, middleware, index + 1
        )

        nextMiddleware({ ...context, next: nextPipeline })

    }
}

export default middlewarePipeline

MiddlewarePipeline принимает три аргумента:

context: это объект контекста, который мы создали ранее, чтобы его можно было передать каждому промежуточному программному обеспечению в стеке.

middleware: это сам массив middleware, определенный в поле route.

index: это индекс текущего middleware, запускаемого в массиве middleware.

const nextMiddleware = middleware[index]
if(!nextMiddleware){
  return context.next
}

Здесь мы просто извлекаем middleware с помощью индекса, который был передан в функцию middlewarePipeline. Если middleware не найдено по этому индексу, запускается next.

return () => {
  const nextPipeline = middlewarePipeline(
    context, middleware, index + 1
  )
  nextMiddleware({ ...context, next: nextPipeline })
}

Тут мы вызываем nextMiddleware для запуска при передаче в контексте, а затем const nextPipeline. Важно отметить, что функция middlewarePipeline представляет собой рекурсивную функцию, которая будет вызывать себя для извлечения следующего middleware, которое будет выполняться в стеке, при увеличении индекса на 1.

Собираем все вместе

Давайте воспользуемся нашим middleware. Отредактируйте файл src/router/router.js следующим образом:

import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'

import Login from '../components/Login'
import Dashboard from '../components/Dashboard'
import Movies from '../components/Movies'

import guest from './middleware/guest'
import auth from './middleware/auth'
import isSubscribed from './middleware/isSubscribed'
import middlewarePipeline from './middlewarePipeline'


Vue.use(Router)

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [{
            path: '/login',
            name: 'login',
            component: Login,
            meta: {
                middleware: [
                    guest
                ]
            }
        },

        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
                middleware: [
                    auth
                ]
            },
            children: [{
                path: '/dashboard/movies',
                name: 'dashboard.movies',
                component: Movies,
                meta: {
                    middleware: [
                        auth,
                        isSubscribed
                    ]
                }
            }],
        }
    ]
})

router.beforeEach((to, from, next) => {
    if (!to.meta.middleware) {
        return next()
    }
    const middleware = to.meta.middleware

    const context = {
        to,
        from,
        next,
        store
    }


    return middleware[0]({
        ...context,
        next: middlewarePipeline(context, middleware, 1)
    })

})

export default router

Здесь мы используем middlewarePipeline для запуска последующих middleware, содержащихся в стеке.

return middleware[0]({
  ...context,
  next: middlewarePipeline(context, middleware, 1)
})

По поводу данного момента есть интересный комментарий

После вызова первого middleware с использованием функции middlewarePipeline также вызываются последующие middleware, содержащиеся в стеке, до тех пор, пока промежуточные программы не закончиться.

Если вы посещаете маршрут /dashboard/movies, вы должны быть перенаправлены на маршрут /dashboard. Это связано с тем, что user в настоящее время authenticated, но не имеет активной подписки. Установите для свойства store.state.user.isSubscribeed в нашем store на значение true. Теперь вы должны иметь доступ к /dashboard/movies.

Заключение

Промежуточное ПО (Middlewares) – отличный способ защиты различных маршрутов в приложении. Это очень простая реализация того, как вы можете использовать несколько middlewares для защиты одного маршрута в вашем приложении Vue. Вы можете найти ссылку на репозиторий Github здесь.

Оригинальная статья: Dotun Jolaoso Understanding Vue middleware pipelines

Была ли вам полезна эта статья?
[17 / 5]

Spread the love
Подписаться
Уведомление о
guest
10 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Игорь
Игорь
4 лет назад

А что если нет middleware?

Игорь
Игорь
4 лет назад
Reply to  Editorial Team

Я имел в виду что если его нет в роутере

Игорь
Игорь
4 лет назад
Reply to  Игорь

Был невнимателен!

if (!to.meta.middleware) {
return next()
}

Сергей
Сергей
4 лет назад

В статье ошибка. А конкретно в этом месте:
return middleware[0]({
…context,
next: middlewarePipeline(context, middleware, 1)
})
Вы переобозначаете функцию next из контекста какой-то сторонней, из-за чего невозможно выйти из какого-то Middleware не зайдя в следующий. Нужно сделать так:
В beforeEach:

return middleware[0]({
…context,
nextMiddleware: middlewarePipeline(context, middleware, 1)
})

И в middlewarePipeline:

return () => {
const nextPipeline = middlewarePipeline(
context, middleware, index + 1
)
nextMiddleware({ …context, nextMiddleware: nextPipeline })
}

И описывать сами middleware вот так:

export default function auth ({ next, store, nextMiddleware }){
if(!store.getters.auth.loggedIn){
return next({
name: ‘login’
})
}
return nextMiddleware()
}

Только тогда это будет работать как нужно. Т.е. при ошибке он вызовет функцию next vue-router и перейдет по ней, а если нет, то вызовет nextMiddleware. А то получалось так, что он вызывал next (в моем случае ту, которая nextMiddleware) и постоянно пробегался по всему массиву с middleware, пока не дойдет до конца и не вызовется

if(!nextMiddleware){
return context.next
}

Т.е. вообще не так, как должно было работать.
Поправьте статью, пожалуйста, а то не все читают комменты.

edteam
Администратор
4 лет назад
Reply to  Сергей

Спасибо за комментарий. Так как это перевод думаю вносить правки в код наверно не стоит. Но добавлю в статью примечание на этот комментарий.

Spo
Spo
4 лет назад
Reply to  Сергей

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

Максим
Максим
4 лет назад
Reply to  Сергей

На самом деле это ошибка и в то же время не ошибка, ибо если использовать next() только тогда, когда middleware пройдет все проверки и надо двигаться дальше, а если middleware проверку не прошел, то достаточно использовать функции router.push или взять redirect из контекста, чтобы куда нибудь перебросить пользователя. Но ваше решение тоже имеет право на жизнь, тут кому как удобнее. 🙂

veltor
veltor
2 лет назад
Reply to  Сергей

В статье ошибка, но совсем не в том месте про которое вы пишите, а в auth.js (в оригинале к слову написано верно)

export default function auth({ next, router }) {
  if (!localStorage.getItem('jwt')) {
    return router.push({ name: 'login' }); // <--
  }

  return next();
}
Анонимно
Анонимно
3 лет назад

Огонь! Спасибо