Это руководство представляет собой вскрытие моего опыта написания vue-browser-acl, автономного компонента Vue.js, который переносит ACL (уровень контроля доступа) в браузер в виде простой всеобъемлющей директивы.

В конце этой статьи мы получим директиву Vue, которая выглядит примерно так:

<button v-can:delete="post">Delete</button>

Мы рассмотрим эти темы:

  • Аргументы и модификаторы директив
  • Жизненный цикл директивы

Это не статья о передовой практике ACL, а руководство о том, как сделать код более читабельным и приятным для других и для себя в будущем.

Мотивация

Vue предоставляет вам v-if и v-show из коробки, и я уверен, что ваши шаблоны, как и мой, завалены этими двумя директивами.

Они эквивалентны структуре управления if-else, которую мы знаем из нашего кода, и, что неудивительно, мы часто оказываемся в долгих и глубоких условиях, которые вынуждают вас остановиться, чтобы прочитать их, чтобы понять.

<button v-if="job.users.contains(user) && user.isManager()">Delete</button>

Часто во Vue вы извлекаете их в вычисляемые свойства или метод компонента, чтобы скрыть сложность:

export default {
  methods: {
    canDelete(job) {
      return this.job.users.contains(this.user) &&
        this.user.isManager()
    }
  }
}

Это позволяет упростить состояние кнопки:

<button v-if="canDelete(job)">Delete</button>

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

Вмешательства - отличный способ использовать дублирование кода. Вы также можете расширить прототип vue, как мы уже много лет делали это в JavaScript. (Я буду называть оба подхода смешанными для простоты). Mix-ins позволяют нам объединить весь код доступа в одном месте и сделать его доступным для всех компонентов Vue. Например, мы могли бы добавить $can микс (на самом деле, мы сделаем это позже) и теперь трансформируем кнопку примерно так:

<button v-if="$can('delete', job)">Delete</button>

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

ACL Primer

Сначала я кратко расскажу о наших требованиях. ACL в браузере в основном используется для UX-целей и не заменяет проверку разрешений в серверной части.

Есть много подходов к ACL. Существуют разные уровни детализации и способы выражения разрешений, но в основном это сводится к определению того, как пользователь, возможно, через роль, может выполнять определенные действия (или команды) над моделями в вашем приложении. Действия могут быть любыми: создавать, просматривать, редактировать, удалять, передавать, отправлять, управлять участниками и т. Д.

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

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

acl.rule('edit', Post, (user, post) => {
  return user.id === post.userId
})

Дело в том, что для глагола edit нам понадобится экземпляр сообщения, чтобы определить разрешение.

С другой стороны, для такого глагола, как создать, мы не сможем предоставить экземпляр, поскольку сообщение еще не существует. Но нам нужно будет что-то передать, чтобы ACL мог определить, должен ли пользователь создать сообщение:

acl.rule('create', Post, (user) => {
  return user.isRegistered()
})

Итак, в этом случае мы можем передать либо класс Post, либо строку «Post» вместо фактического экземпляра.

В приведенном выше примере действие create может быть выполнено только в том случае, если пользователь зарегистрирован. Важным моментом является то, что аргумент Post используется только для указания того, что правило касается сообщений, и не используется в коде, определяющем разрешение; только пользователь есть.

Это не так уж важно, но поможет нам в некоторых решениях, которые мы сделаем при разработке директивы v-can в будущем.

Vue.use ()

Как директивы, так и микшеры подключаются к Vue с помощью вызова use(plugin). Ожидается объект с методом install. Vue вызывает метод и включает себя в качестве первого аргумента. И каждый аргумент, который вы передаете use после этого, также будет передан для установки. (см. раздел плагины)

Vue.use({
  install(Vue, options, moreOptions, evenMoreOptions) {
    ...
  }
})

Определение правил (вариантов)

Для этой конкретной директивы пользователю плагина необходимо:

  • Предоставить доступ текущему пользователю
  • Определите некоторые правила, которые являются основой для работы ACL.

Есть несколько способов сделать это.

Вы можете установить все правила и конфигурации для объекта, а затем передать его готовым с правилами и всем в Vue, например: Vue.use(instance). Это то, что делает vue-router.

Вы также можете просто передать плагин, а затем выполнить всю настройку в функции установки. Однако мы хотим скрыть «беспорядок», связанный с импортом и созданием экземпляра Acl, но нам все равно нужно открыть этот экземпляр, чтобы пользователь плагина мог добавлять к нему правила.

Это могло выглядеть примерно так:

import Acl from 'vue-browser-acl'
Vue.use(Acl, user, acl => {
  acl.rule('edit', 'Post', (user, post) => {
    return user.id === post.userId
  })
})

Во-первых, мы передаем наш плагин Acl (помните, что плагин - это просто объект с функцией установки), а во-вторых, мы передаем user. Нет никаких требований относительно того, что такое пользователь. Это может быть электронное письмо, объект, роль или даже значение null. Мы будем использовать это позже, чтобы перейти к функции can() Acl.

Третий параметр - это обратный вызов, который предоставляет экземпляр acl (код, который нам все еще нужно написать) в качестве первого и единственного параметра.

Использование обратного вызова - это способ раскрыть то, что важно для настройки плагина, и скрыть все остальное.

В приведенном выше коде мы определяем одно правило - правило для проверки, может ли пользователь редактировать сообщение или нет.

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

Давайте посмотрим на наш плагин и на то, как мы можем предоставить предлагаемый API.

import Acl from 'browser-acl'
export default {
  install: function (Vue, user, callback, options) {
    const acl = new Acl(options)
    callback(acl)
  }
}

Это действительно все, что нужно. Обратите внимание, что я добавил параметр для третьего аргумента на тот случай, если вы хотите передать параметры в базовый модуль ACL.

Вспомогательная функция

Теперь наш ACL подключен, но мы по-прежнему ничего не можем с ним сделать. Давайте добавим вспомогательную функцию перед тем, как приступить к директиве.

// index.js
import Acl from 'browser-acl'
export default {
  install: function (Vue, user, callback, options) {
    const acl = new Acl(options)
    callback(acl)
    Vue.prototype.$can = (...args) => acl.can(user, ...args)
  }
}

Добавить функцию помощи очень просто. Мы добавляем функцию $can к Vue.prototype, после чего все экземпляры Vue могут получить доступ к вспомогательной функции. Это означает, что мы уже используем функцию:

<button v-if="$can('edit', post)">Edit</button>

Важно отметить, что post должен существовать как данные (или вычисленное значение) в компоненте или должен ссылаться на переменную в цикле (v-for="post in posts").

Теперь, когда функциональность ACL создана, пора создать директиву.

Директивы 101

Документация для директив довольно хороша, поэтому я не буду вдаваться в подробности, но я расскажу только о тех частях, которые относятся к нашей директиве can и, вероятно, к большинству директив, которые вы когда-либо напишете сами.

Это был пример, с которого мы начали - в сопровождении нескольких других знакомых «лиц»:

<button v-can:delete="post">Delete</button>
<input v-on:keyup.enter.prevent="validate">
<a class="btn btn-link" @click.prevent="save">Save</a>

Чтобы разбить это более подробно, приведенные выше директивы:

v-can с аргументом delete и выражением значения post.

v-on с аргументом keyup и модификаторами enter и prevent.

@click является синтаксическим сахаром для v-on:click, но, как таковой, действует как директива с модификатором prevent.

Имя директивы - это имя без v-, и вы регистрируете их следующим образом: can, on

Директива может иметь единственный аргумент, который всегда следует после имени директивы. Для v-on аргумент используется, чтобы указать, какое событие следует прослушивать: keyup, click, чтобы назвать несколько.

У директивы может быть несколько модификаторов. Модификаторы похожи на флаги, логические значения, на которые ваша директива может реагировать. Для v-on модификаторы типа prevent и stop используются для вызоваpreventDefault и stopProrogation соответственно в событии при запуске.

Модификаторы также могут использоваться как передача дополнительных аргументов. Хотя это не является предполагаемым использованием, это, безусловно, общепринятая практика, как видно из приведенного выше примера в случае enter. Модификаторы будут доступны в виде объекта со значением true, например {enter: true, prevent: true}. Поскольку они преобразуются в объект с именами модификаторов в качестве ключей, существуют ограничения на то, как вы можете использовать модификаторы в качестве дополнительных аргументов:

  • Нет гарантии заказа
  • У вас не может быть повторяющихся значений

Итак, если, например, мы создавали директиву медиа-запроса и хотели использовать модификаторы для аргументов:

<div v-mq.min.764.max.1024>Content only shown for medium devices</div>

Это не сработает, поскольку мы не будем знать, относится ли min или max к 764 или 1024. Вам придется придумать какую-то хитрую логику, чтобы заставить ее работать хотя бы.

v-can

(а мы можем?)

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

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

Итак, для директивы мы должны определить синтаксис, который охватывает следующее: глагол, объект, параметр скрыть (по умолчанию) и параметр возможность отключить.

Мы могли бы обернуть все это в объект a и передать его как значение директиве, но, конечно, мы хотим использовать аргументы и модификаторы директивы.

hide и disable являются взаимоисключающими, поэтому может быть только одно значение, которое является хорошим кандидатом в качестве аргумента: v-can:hide или v-can:disable.

Однако, вспоминая v-on:keyup, кажется, что этот аргумент дополняет имя директивы для описания события «при нажатии клавиши». Он прекрасно описывает, что делает директива: привязка к тому, что происходит, когда клавиша отпускается.

Возвращаясь к v-can:hide, мы получаем «можно скрыть», что на самом деле не то, о чем наша директива. Мы хотим, чтобы он читал, что может делать пользователь, или, скорее, что пользователь должен уметь делать, чтобы элемент был показан или включен.

Глаголы тоже по большей части являются аргументами в единственном числе. Для сообщений можно использовать следующие глаголы: создать, изменить, удалить и комментарий. Эти глаголы можно перевести в употребление, например, v-can:create, v-can:edit и так далее. Это очень хорошо читается: «может создавать» и «может редактировать». Нет сомнений в том, о чем идет речь.

Остается скрыть и отключить, а также объект (сообщение). Для объекта у нас действительно нет выбора; это должна быть ценность.

Поскольку мы уже отошли от идеи предоставления аргументов в виде объекта значения, это означает, что hide и disable должны быть модификаторами. Что на самом деле не так уж и плохо. «Следует скрыть» или «следует отключить» - это логические значения. Идеально.

Это оставляет нам:

<button v-can:edit.disable="post">Edit</button>
  • Глагол - это аргумент
  • Hide / disable - взаимоисключающие модификаторы
  • Объект - это выражение значения

Примечание. Опубликованный пакет поддерживает другие варианты, которые в некоторых случаях имеют больше смысла - например, вы можете написать v-can="'create Post'", и есть возможность передавать в правила глагол, объект и дополнительные аргументы, используя нотацию массива. Кроме того, у него есть модификаторы, которые работают с коллекцией объектов.

Реализация

Мы добавляем директивы с помощью Vue.directive(). Он принимает в качестве первого аргумента имя аргумента без префикса v-. Так что в нашем случае просто can.

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

Функция будет вызываться изначально при создании содержащего элемента и впоследствии будет вызываться при изменении данных. Требуется три аргумента function (el, binding, vnode), из которых bindings наиболее интересны для нас. el - это элемент DOM, а vnode - виртуальный dom-узел Vue (оттуда вы можете получить доступ к свойствам данных).

аргумент привязки дает вам доступ к аргументу, модификаторам, значению и т. Д.

  • name: имя без префикса v-.
  • value: значение после вычисления выражения (вы также можете получить само выражение)
  • arg: аргумент
  • modifiers: объект с именем модификатора для ключа и истиной для значения

Полный список свойств привязки см. В документации.

...
Vue.directive('can', function (el, bindings, vnode) {
  const behaviour = binding.modifiers.disable ? 'disable' : 'hide'
  const ok = acl.can(user, binding.arg, binding.value)
  if (!ok) {
    if (behaviour === 'hide') {
      commentNode(el, vnode)
    } else if (behaviour === 'disable') {
      el.disabled = true
    }
  }
})
...

По умолчанию, чтобы скрыть: сначала мы проверяем, присутствует ли модификатор disable. Если нет, то по умолчанию скрывается, так же работаетv-if.

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

В случае, если пользователь не авторизован, например чтобы отредактировать сообщение, мы обрабатываем элемент соответствующим образом. Если пользователь авторизован, мы ничего не делаем.

Самый простой случай - отключить. Мы включаем свойство disabled элемента. Это эквивалент:

<button disabled>Edit</button>

Для hide все немного сложнее. Основная идея состоит в том, что вы заменяете содержимое пустым комментарием <!-- —->. Вы можете посмотреть код здесь.

Вот и все. Это реализация директивы.

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

Жизненный цикл директивы

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

Так же, как у компонента Vue есть хуки жизненного цикла (created, connected, before * и т. Д.), Так и директивы. Однако для большинства директив вы будете использовать только две из них: bind и update. Считайте их аналогами монтироваться и beforeUpdate для компонентов соответственно.

Во многих случаях вам может потребоваться такое же поведение для bind и update, но не заботиться о других хуках.

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

Vue.directive('can', canImplementation)

эквивалентно:

Vue.directive('can', {
  bind: ourImplementation,
  update: ourImplementation
})

Привязка происходит один раз, когда директива была связана с элементом (el), после чего она больше не вызывается.

Обновление происходит, когда данные изменяются из-за реактивности Vue - из-за пользовательского ввода или какого-либо побочного эффекта. Для v-can объект нашего ACL - это то, что может измениться. Скажем, сообщение, привязанное к компоненту, меняется на другое сообщение, тогда ACL повторно оценивает разрешения пользователя.

Примечание. Перехватчик обновления принимает четвертый аргумент oldVnode, а объект привязок также включает bindings.oldValue. Это позволяет сравнивать старые и новые значения, чтобы избежать ненужных вычислений.

Другие перехватчики: componentUpdated, insert и unbind.

Отменить привязку аналогично beforeUnmount и beforeDestroy. Это позволяет вам должным образом снести любые созданные вами объекты. Скажем, например, вы делаете директиву, которая будет воспроизводить звук при наведении курсора. Перед тем, как компонент (а значит, и директива) будет удален, вам нужно будет остановить воспроизведение звука. В противном случае он продолжал бы жить в браузере, занимая аппаратный аудиоканал.

См. Документацию Vue для insert и componentUpdated.

Что дальше?

В качестве упражнения попробуйте увидеть, можете ли вы реализовать строковый вид, чтобы можно было написать: v-can="'edit post'" или v-can="'create Post'" для экземпляров и классов соответственно.

Подсказка: вам может понадобиться vnode.context

Для меня это было упражнением в том, чтобы поделиться своим опытом разработки этой директивы Vue. Вопросы и отзывы приветствуются.

Если вы зашли так далеко, спасибо за внимание :)

P.S. Обязательно ознакомьтесь с poi и poi-preset-karma, которые позволяют разрабатывать и тестировать компоненты Vue с нулевой конфигурацией.