- PVSM.RU - https://www.pvsm.ru -
При разработке приложений на React довольно неудобно создавать независимые друг от друга компоненты, т.к. стандартным способом для обмена данными между ними является "Lifting State Up [1]". Этот подход постепенно загрязняет ненужными свойствами промежуточные компоненты, делая их неудобными для повторного использования.
Наиболее популярными средствами решения этой проблемы (и некоторых других) являются такие библиотеки как Redux и Mobx, позволяющие хранить данные в отдельном месте и передавать их компонентам напрямую. В этой статье я хочу продемонстрировать наш подход к решению данного вопроса.
Отдельная страница в СЭД Docsvision собирается в специальном WYSIWYG-редакторе из множества React-компонентов, помещенных на нужные позиции:
Партнеры могут писать свои JavaScript-сценарии, которые взаимодействуют с компонентами через их API (читают/записывают свойства, вызывают методы), а могут рендерить эти же компоненты через обычный 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.
Наиболее часто используемыми для свойств декораторами оказались:
Название декоратора | Описание |
---|---|
@r |
Свойство доступно только для чтения, не позволяя изменить его. |
@rw |
Свойство доступно как для чтения, так и для записи. |
@apiEvent |
Указывает, что мы должны рассматривать значение свойства как обработчик для событий компонента с аналогичным именем. Также, при работе с такими свойствами мы реализовываем специфическую для событий логику (к примеру, автоматическую отписку предыдущего обработчика при установке нового значения свойства). |
@handler(paramName) |
В отличии от перечисленных выше, этот декоратор вешается не на свойство, а на любой геттер или сеттер внутри компонента. Это позволяет добавлять свою логику при записи или чтении значения свойства. Например, обрезание пробелов с начала и конца значения:
|
При этом, сами декораторы обычно не содержат какой-либо бизнес-логики, а лишь сохраняют информацию о том, что за декоратор был применён. Это делается с помощью библиотеки 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 + " доступно только для чтения.");
}
}
}
}
Таким образом, мы получили универсальное 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
Нажмите здесь для печати.