Перейти к содержимому

Расширение фреймворка

Alpine имеет очень открытую кодовую базу, которая позволяет расширять её различными способами. Фактически, каждая доступная директива и магическое свойство в самом Alpine используют именно эти API. Теоретически вы можете восстановить всю функциональность Alpine, используя их самостоятельно.

Проблемы жизненного цикла

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

Поскольку эти API влияют на то, как Alpine инициализирует страницу, их необходимо зарегистрировать ПОСЛЕ того, как Alpine будет загружен и доступен на странице, но ДО того, как он инициализирует саму страницу.

Существует два разных метода в зависимости от того, импортируете ли вы Alpine в пакет или включаете его напрямую через тег <script>. Давайте рассмотрим оба.

Через тег script

Если вы включаете Alpine через тег <script>, вам нужно будет регистрировать любой пользовательский код расширения внутри слушателя событий alpine:init.

Вот пример:

<html>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('foo', ...)
})
</script>
</html>

Если вы хотите извлечь код расширения во внешний файл, обязательно убедитесь, что тег <script> файла расположен ДО подключения Alpine, например:

<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
</html>

Через модуль NPM

Если вы импортировали Alpine в пакет, вам необходимо убедиться, что вы регистрируете любой код расширения ПОСЛЕ импорта глобального объекта Alpine и ПЕРЕД вызовом Alpine.start() для инициализации Alpine. Например:

import Alpine from 'alpinejs'
Alpine.directive('foo', ...)
window.Alpine = Alpine
window.Alpine.start()

Теперь, когда мы знаем, как подключать API расширений, давайте более подробно рассмотрим, как использовать каждый из них.

Пользовательские директивы

Alpine позволяет вам регистрировать ваши собственные директивы с помощью API Alpine.directive().

Сигнатура метода

Alpine.directive(
'[name]',
(el, { value, modifiers, expression }, { Alpine, effect, cleanup }) => {}
);
ПараметрОписание
nameИмя директивы. Например, имя «foo» будет использоваться как x-foo
elЭлемент DOM, к которому добавляется директива
valueЕсли предусмотрено, часть директивы после двоеточия. Пример: 'bar' в x-foo:bar
modifiersМассив дополнений к директиве, разделенных точками. Пример: ['baz', 'lob'] из x-foo.baz.lob
expressionЧасть значения атрибута директивы. Пример: law из x-foo="law"
AlpineГлобальный объект Alpine
effectФункция для создания реактивных эффектов, которые будут автоматически очищаться после удаления этой директивы из DOM.
cleanupФункция, в которую вы можете передать специальные обратные вызовы, которая будет выполняться, когда эта директива будет удалена из DOM.

Простой пример

Вот пример простой директивы, которую мы собираемся создать, под названием x-uppercase:

<div x-data>
<span x-uppercase>Привет, мир!</span>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('uppercase', (el) => {
el.textContent = el.textContent.toUpperCase();
});
});
</script>
Привет, мир!

Выполнение выражений

При регистрации пользовательской директивы вам может потребоваться выполнить предоставленное пользователем выражение JavaScript:

Например, предположим, что вы хотите создать специальную директиву в качестве ярлыка для console.log(). Что-то вроде:

<div x-data="{ message: 'Привет, мир!' }">
<div x-log="message"></div>
</div>

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

К счастью, Alpine предоставляет свою систему для выполнения выражений JavaScript с помощью API evaluate(). Вот пример:

Alpine.directive('log', (el, { expression }, { evaluate }) => {
// expression === 'message'
console.log(evaluate(expression));
});

Теперь, когда Alpine инициализирует <div x-log...>, он извлекает выражение, переданное в директиву (в данном случае «message»), и рассчитывает его в контексте области действия компонента Alpine текущего элемента.

Представляем реактивность

Основываясь на предыдущем примере с x-log, предположим, что мы хотим, чтобы x-log регистрировал значение message, а также регистрировал его, если значение изменяется.

Учитывая следующий шаблон:

<div x-data="{ message: 'Привет, мир!' }">
<div x-log="message"></div>
<button @click="message = 'yolo'">Изменить</button>
</div>

Мы хотим установить начальное значение переменной message как «Привет, мир!», а при нажатии на <button> задаём новое значение — «yolo».

Для этого мы можем скорректировать реализацию x-log и ввести два новых API: evaluateLater() и effect():

Alpine.directive('log', (el, { expression }, { evaluateLater, effect }) => {
let getThingToLog = evaluateLater(expression);
effect(() => {
getThingToLog((thingToLog) => {
console.log(thingToLog);
});
});
});

Давайте построчно пройдемся по приведённому выше коду.

let getThingToLog = evaluateLater(expression);

Здесь вместо того, чтобы сразу анализировать message и извлекать результат, мы преобразуем строковое выражение («message») в фактическую функцию JavaScript, которую можно запустить в любой момент. Если вы собираетесь анализировать выражение JavaScript более одного раза, настоятельно рекомендуется сначала сгенерировать функцию JavaScript и использовать её, а не вызывать evaluate() напрямую. Причина в том, что процесс интерпретации обычной строки как функции JavaScript является дорогостоящим и его следует избегать, когда в этом нет необходимости.

effect(() => {
...
})

Передавая в effect() обратный вызов, мы указываем Alpine на необходимость немедленного выполнения этого вызова, а затем отслеживания всех зависимостей, которые он использует (свойства x-data, например, message в нашем случае). Теперь, как только одна из зависимостей изменится, этот обратный вызов будет запущен повторно. Это и дает нам нашу «реактивность».

Вы можете узнать эту функциональность из x-effect. Под капотом находится один и тот же механизм.

Вы также можете удивиться, почему мы здесь не используем Alpine.effect(). Причина в том, что функция effect, передаваемая через параметр method, имеет особую функциональность, которая очищается при удалении директивы со страницы по какой-либо причине.

Например, если элемент с x-log был удалён со страницы, то при изменении свойства message с помощью effect() вместо Alpine.effect() значение больше не будет выводиться в консоль.

getThingToLog((thingToLog) => {
console.log(thingToLog);
});

Теперь вызовем getThingToLog, который, если вы помните, является собственно JavaScript-функцией, представляющей собой версию строкового выражения: «message».

Можно было бы ожидать, что getThingToCall() сразу же вернет результат, но вместо этого Alpine требует передать обратный вызов для получения результата.

Причиной этого является поддержка асинхронных выражений, таких как await getMessage(). Передавая обратный вызов «получателя» вместо немедленного получения результата, вы также позволяете своей директиве работать с асинхронными выражениями.

Очистка

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

Alpine упрощает эту задачу, предоставляя вам функцию «очистки» при регистрации пользовательских директив.

Вот пример:

Alpine.directive('...', (el, {}, { cleanup }) => {
let handler = () => {};
window.addEventListener('click', handler);
cleanup(() => {
window.removeEventListener('click', handler);
});
});

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

Пользовательский порядок

По умолчанию любая новая директива будет выполняться после большинства стандартных (за исключением x-teleport). Обычно это приемлемо, но иногда вам может потребоваться запустить пользовательскую директиву перед другой конкретной. Этого можно добиться, связав функцию .before() с Alpine.directive() и указав, какая директива должна выполняться после вашей пользовательской.

Alpine.directive('foo', (el, { value, modifiers, expression }) => {
Alpine.addScopeToNode(el, { foo: 'bar' });
}).before('bind');
<div x-data>
<span x-foo x-bind:foo="foo"></span>
</div>

Пользовательская магия

Alpine позволяет вам регистрировать пользовательские «магии» (свойства или методы) с помощью Alpine.magic(). Любая магия, которую вы зарегистрируете, будет доступна для всего кода Alpine вашего приложения с префиксом $.

Сигнатура метода

Alpine.magic('[name]', (el, { Alpine }) => {});
ПараметрОписание
nameИмя магического метода. Например, имя «foo» будет использоваться как $foo
elЭлемент DOM, из которого была активирована магия
AlpineГлобальный объект Alpine

Магические свойства

Вот базовый пример магического помощника «$now», позволяющего легко получить текущее время из любой точки Alpine:

<span x-data x-text="$now"></span>
<script>
document.addEventListener('alpine:init', () => {
Alpine.magic('now', () => {
return new Date().toLocaleTimeString();
});
});
</script>

Теперь тег <span> будет содержать текущее время, что-то вроде «12:00:00 PM».

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

Благодаря этому вы можете реализовать магические «функции», возвращая функцию из геттера.

Магические функции

Например, если бы мы хотели создать магическую функцию $clipboard(), которая принимает строку для копирования в буфер обмена, мы могли бы реализовать её следующим образом:

Alpine.magic('clipboard', () => {
return (subject) => navigator.clipboard.writeText(subject);
});
<button @click="$clipboard('привет, мир')">Копировать «Привет, мир»</button>

Теперь, когда доступ к $clipboard возвращает саму функцию, мы можем немедленно вызвать её и передать ей аргумент, как мы видим в шаблоне с $clipboard('привет, мир').

Если хотите, вы можете использовать более краткий синтаксис (функция с двойной стрелкой) для возврата функции из функции:

Alpine.magic('clipboard', () => (subject) => {
navigator.clipboard.writeText(subject);
});

Написание и обмен плагинами

Теперь вы должны убедиться, насколько удобно и просто регистрировать свои собственные директивы и магические функции в вашем приложении, но как насчёт того, чтобы поделиться этой функциональностью с другими через пакет NPM или что-то в этом роде?

Вы можете быстро начать работу с официальным пакетом плагинов Alpine. Это так же просто, как клонировать репозиторий и запустить npm add && npm run build, чтобы создать плагин.

В демонстрационных целях давайте создадим с нуля воображаемый плагин Alpine под названием Foo, который включает в себя как директиву (x-foo), так и магию ($foo).

Мы начнём создавать этот плагин для использования в виде простого тега <script> вместе с Alpine, затем выровняем его до модуля для импорта в пакет:

Включение через script

Давайте начнём в обратном порядке и посмотрим, как наш плагин будет включен в проект:

<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-init="$foo()">
<span x-foo="'привет, мир'">
</div>
</html>

Обратите внимание, что наш скрипт включен ДО самого Alpine. Это важно, иначе Alpine уже будет инициализирован к моменту загрузки нашего плагина.

Теперь давайте заглянем внутрь содержимого /js/foo.js:

document.addEventListener('alpine:init', () => {
window.Alpine.directive('foo', ...)
window.Alpine.magic('foo', ...)
})

Вот и всё! Создание плагина для включения через тег сценария с Alpine чрезвычайно просто.

Пакетный модуль

Теперь предположим, что вы хотите создать плагин, который кто-то сможет установить через менеджер пакетов и включить в свой пакет.

Как и в последнем примере, мы рассмотрим всё в обратном порядке, начиная с того, как будет выглядеть использование этого плагина:

import Alpine from 'alpinejs';
import foo from 'foo';
Alpine.plugin(foo);
window.Alpine = Alpine;
window.Alpine.start();

Здесь вы заметите новый API: Alpine.plugin(). Это удобный метод, который Alpine предоставляет, чтобы пользователям вашего плагина не приходилось самостоятельно регистрировать несколько различных директив и магических функций.

Теперь давайте посмотрим на исходный код плагина и на то, что экспортируется из foo:

export default function (Alpine) {
Alpine.directive('foo', ...)
Alpine.magic('foo', ...)
}

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

Затем вы можете расширять Alpine по своему усмотрению.

Проверка знаний

  1. Порядок регистрации расширения при использовании сборки Alpine…

  2. Как зарегистрировать хелпер $now для вывода текущего времени?

    <span x-data x-text="$now"></span>
    <script>
    document.addEventListener('alpine:init', () => {
    // код расширения
    });
    </script>
  3. Как создать свою директиву x-lowercase?

    <div x-data>
    <span x-lowercase>НЕНАВИЖУ КАПС!</span>
    </div>
    <script>
    document.addEventListener('alpine:init', () => {
    // определение директивы
    });
    </script>