- PVSM.RU - https://www.pvsm.ru -

Как мы общаемся с React-компонентам при помощи декораторов в TypeScript

При разработке приложений на React довольно неудобно создавать независимые друг от друга компоненты, т.к. стандартным способом для обмена данными между ними является "Lifting State Up [1]". Этот подход постепенно загрязняет ненужными свойствами промежуточные компоненты, делая их неудобными для повторного использования.

image

Наиболее популярными средствами решения этой проблемы (и некоторых других) являются такие библиотеки как Redux и Mobx, позволяющие хранить данные в отдельном месте и передавать их компонентам напрямую. В этой статье я хочу продемонстрировать наш подход к решению данного вопроса.

Отдельная страница в СЭД Docsvision собирается в специальном WYSIWYG-редакторе из множества React-компонентов, помещенных на нужные позиции:

image

Партнеры могут писать свои JavaScript-сценарии, которые взаимодействуют с компонентами через их API (читают/записывают свойства, вызывают методы), а могут рендерить эти же компоненты через обычный JSX-синтаксис (например, внутри каких-то модальных окон или собственных компонентах).

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

  1. Получение параметров с сервера, настроенных в WYSIWYG-редакторе.
  2. Взаимодействие с помощью JavaScript-сценариев.
  3. Взаимодействие через JSX.

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

// Получаем нужный компонент по названию из нашего хранилища компонентов.
let textBox = layout.controls.textBox;

// Изменяем значение полученного компонента через механизм параметров.
textBox.params.value = "Мой текст";
// Приводим значение в верхний регистр при изменении.
textBox.params.dataChanged = (sender, data)=> sender.params.value = data.newValue.toUpperCase();

А так выглядит сам класс для работы с параметрами у компонента TextBox:

class TextBoxParams {
    /** Значение. */
    @rw value?: string = '';
    /** Можно ли редактировать компонент. */
    @r canEdit?: boolean;
    /** Событие, возникающее при изменении значения. */
    @apiEvent dataChanged?: BasicApiEvent<string>;
}

Как мы видим, кроме обычного перечисления свойств, как в стандартном механизме свойств, здесь имеются ещё и декораторы @r, @rw и @apiEvent. С их помощью мы создаем более гибкое поведение для наших свойств.

И так как этот же класс используется и в качестве интерфейса для React-свойств, то мы можем одинаково взаимодействовать с компонентом как внешними скриптами, так и через в JSX.

Как мы общаемся с React-компонентам при помощи декораторов в TypeScript - 3

Наиболее часто используемыми для свойств декораторами оказались:

Название декоратора Описание
@r
Свойство доступно только для чтения, не позволяя изменить его.
@rw
Свойство доступно как для чтения, так и для записи.
@apiEvent
Указывает, что мы должны рассматривать значение свойства как обработчик для событий компонента с аналогичным именем. Также, при работе с такими свойствами мы реализовываем специфическую для событий логику (к примеру, автоматическую отписку предыдущего обработчика при установке нового значения свойства).
@handler(paramName)
В отличии от перечисленных выше, этот декоратор вешается не на свойство, а на любой геттер или сеттер внутри компонента. Это позволяет добавлять свою логику при записи или чтении значения свойства. Например, обрезание пробелов с начала и конца значения:

class TextBoxParams {
    /** Значение. */
    @rw value?: string = '';
}

class TextBox extends BaseControl<TextBoxProps, TextBoxState> {
    ...
    @handler('value')
    private get value(): string {
        return this.state.value.trim();
    }
    ...
}

При этом, сами декораторы обычно не содержат какой-либо бизнес-логики, а лишь сохраняют информацию о том, что за декоратор был применён. Это делается с помощью библиотеки reflect-metadata [2] и удобно тем, что появляется возможность хранить логику в другом месте, гибко объединяя несколько привязанных метаданных. Рассмотрим использование этой библиотеки на упрощённом примере с декоратором @r:

// Название ключа для хранения метаданных касательно декоратора @r.
const READONLY_DECORATOR_METADATA_KEY = "CONTOL_PUBLIC_API_READONLY";

// Код декоратора @r, в нём метаданные привязываются к указанному объекту.
export function r(target: Object, propertyKey: string | symbol) {
    Reflect.defineMetadata(READONLY_DECORATOR_METADATA_KEY, true, target, propertyKey);
}

// Функция для более удобного просмотра наличия метаданных @r у объекта.
export function isReadonly(target: Object, propertyKey: string): boolean {
    return Reflect.getMetadata(READONLY_DECORATOR_METADATA_KEY, target, propertyKey);
}

После применения данного декоратора на каком-нибудь свойстве объекта, к этому свойству автоматически привяжутся метаданные с названием «CONTOL_PUBLIC_API_READONLY» и значением true.

Используя такие метаданные, мы можем динамически задавать нужное поведение нашим параметрам (модификаторы доступа, работу с событиями из таблицы выше и т.д.). Пример простейшей реализации приведён под спойлером ниже.

Пример кода с реализацией

class TextAreaParams {
    @r value: string = '';
}

/** См. пункт 1 ниже. */
interface ITextAreaState extends TextAreaParams {
}

class TextArea extends React.Component<TextAreaParams, ITextAreaState> {
    /** См. пункт 2 ниже. */
    params: TextAreaParams = {} as TextAreaParams;

    constructor(props: ITextAreaProps) {
        super(props);

        /** См. пункт 3 ниже. */
        this.state = new TextAreaParams() as ITextAreaState;

        /** См. пункт 4 ниже. */
        for (let propName in this.state) {
            let descriptor = {
                get: () => this.getParamValue(propName),
                set: (value: any) => this.	(propName, value),
                configurable: true,
                enumerable: true
             } as PropertyDescriptor;

            Object.defineProperty(this.params, propName, descriptor);
        }

        /** См. пункт 5 ниже. */
        for (let propName in this.props) {
            this.setParamValue(propName, this.props[propName], true);
        }
    }

    /** См. пункт 6 ниже. */
    componentWillReceiveProps(nextProps: ITextAreaProps) {
        for (let propName in this.props) {
            if (this.props[propName] != nextProps[propName]) {
                this.setParamValue(propName, this.props[propName]);
            }
        }
    }

    /** См. пункт 7 ниже. */
    getParamValue(paramName: string) {
        return this.state[paramName];
    }

    /** См. пункт 8 ниже. */
    setParamValue(paramName: string, value: any, initial: boolean) {
        const readOnly = isReadonly(this.state, paramName);

        if (!readOnly || initial) {
            this.state[paramName] = val;
            this.forceUpdate();
        } else {
            if (this.props[paramName] != value) {
                console.warn("Свойство " + paramName + " доступно только для чтения.");
            }
        }
    }
}

  1. Интерфейс для state компонента наследуется от класса Params, обеспечивая единообразность данных внутри них. Кроме этого, этот же класс используется и в качестве интерфейса для свойств.
  2. Создаем пустой объект для будущей работы с params. Свойства в нём будут заполнены позднее.
  3. Создаем state компонента, который является экземпляром нашего класса для params.
  4. Заполняем свойства params. Как видно, сам объект params не хранит никаких данных, а использует методы getParamValue и setParamValue в качестве геттера и сеттера.
  5. Синхронизируем первоначальные значения props с params.
  6. При поступлении новых значений props, также синхронизируем их с params. Исходя из этого и предыдущего пункта видно, что React-свойства передают свои значения через параметры в state компонента, что позволяет использовать декораторы и для них.
  7. Значение для параметров просто достается из свойства с аналогичным именем из state, т.к. он для нас это «единый источник правды».
  8. При установке нового значения параметра происходит проверка на то, применён ли наш декоратор @r к свойству с помощью созданного выше хелпера isReadonly. Если свойство доступно только для чтения, то в консоли браузера показывается предупреждение об этом и изменения значения не происходит, иначе новые данные просто записываются в state.

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

Надеюсь, демонстрация нашего подхода позволит кому-то упростить своё API для работы с компонентами, а для более подробного ознакомления с декораторами в TypeScript рекомендую статью своего коллеги Темная сторона TypeScript — @декораторы на примерах [3].

Автор: kyoumur

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/275638

Ссылки в тексте:

[1] Lifting State Up: https://reactjs.org/docs/lifting-state-up.html

[2] reflect-metadata: https://github.com/rbuckton/reflect-metadata

[3] Темная сторона TypeScript — @декораторы на примерах: https://habrahabr.ru/company/docsvision/blog/310870/

[4] Источник: https://habrahabr.ru/post/350646/?utm_campaign=350646