Обзор

В smallcase мы стремимся изменить способ инвестирования в Индию. Как видно из заявления о миссии, самое важное здесь — убедиться, что пользователь может инвестировать. Это требует, чтобы платформа smallcase была высокодоступной и надежной в рыночные часы, чтобы пользователи могли успешно совершать сделки (покупать/продавать акции) через свои брокерские счета. Если по какой-либо причине (технической или иной) пользователи не могут инвестировать / совершать транзакции с платформы, нам необходимо исправить это в приоритетном порядке. Если мы не можем исправить это сразу, потому что причины находятся вне нашего непосредственного контроля, мы должны информировать пользователей о проблеме, чтобы уменьшить разочарование пользователей. Эти проблемы нечасты, но возможны. Некоторые примеры вещей, над которыми мы не имеем непосредственного контроля:

  • API обмена по какой-то причине не работает
  • api брокера не работают из-за технического сбоя на их стороне
  • логин брокера не работает с smallcase из-за проблемы на их стороне

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

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

Фон:

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

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

Требования к функциональности продукта и допущения:

  • Эти проблемы носят временный характер и будут решены в течение дня или двух, даже если они находятся вне нашего контроля. Это предположение в целом верно, так как любые серьезные сбои на нашей стороне мы могли бы исправить за день или два, а когда причина вышла из-под нашего контроля, есть много других субъектов (брокеров, бирж), которые были бы пострадавших, для которых решение проблемы было бы так же важно, как и для нас.
  • Настоятельно необходимо убедиться, что мы можем показать срочное объявление о затронутых транзакциях, поэтому нет необходимости иметь возможность показывать несколько объявлений одновременно. Как правило, это справедливо, потому что предполагается, что эта настройка будет использоваться для важных объявлений, связанных с транзакциями, и не может быть ничего более важного, чем это.
  • Не существует простого способа автоматического обнаружения или прогнозирования таких проблем, поэтому в рамках первой итерации мы будем обновлять эти объявления вручную.
  • Нам не нужно повторно развертывать кодовую базу, чтобы включить объявления, поэтому эти объявления должны каким-то образом извлекаться во время выполнения.
  • Эти объявления должны контролироваться продуктом/бизнесом, а не разработчиками, поскольку люди, занимающиеся продуктом, узнают о таких вещах гораздо раньше, чем разработчики. Таким образом, у команды разработчиков должен быть простой интерфейс для включения/отключения этих объявлений или настройки текста каждого объявления.
  • Мы решили, что объявления могут быть двух типов
    Общие объявления, которые будут общими для всех брокеров. Это может произойти, если биржа сталкивается с некоторыми проблемами и, следовательно, затрагивает всех брокеров, или если у нас есть какой-то технический сбой, который влияет на всех брокеров.
    Объявления для конкретных брокеров, которые будут касаться одного брокера и будет отображаться только на соответствующей Брокерской платформе. Это может произойти, если API конкретного брокера не работает или не работает логин.
  • Объявления, специфичные для брокера, будут иметь более высокий приоритет, чем общие объявления, поэтому в случае, если через данные включены как специфичные для брокера, так и общие объявления, объявление, специфичное для брокера, будет отображаться пользователю до тех пор, пока его видимость не будет отключена.
  • На данный момент мы решили, что объявления о блокировке будут отображаться как часть страницы, вверху, и они будут прокручиваемыми, а не липкими. Это было сделано для того, чтобы пользователи не воспринимали объявление как единственную тревожную вещь на каждой странице, потому что, несмотря на то, что транзакции не работают, есть много других функций, таких как отслеживание инвестиций, проверка предыдущих заказов и т. д., которые будет по-прежнему работать, и мы не хотим мешать пользователям делать это.

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

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

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

Откуда взять данные:

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

  1. БД + API: БД была одним из источников, откуда мы могли получить эти данные и показать их пользователям. Но обновлять БД непросто, и люди, не являющиеся техническими специалистами, не могли использовать эту функцию напрямую. Также это создает дополнительную нагрузку на БД, так как нам нужно добавлять/обновлять и удалять объявления, а это не первое, что нам нужно.
  2. Использование статического размещенного файла. Использование статического размещенного файла было еще одним доступным вариантом, когда мы могли разместить файл json в ведре s3, извлекать оттуда данные и показывать данные пользователям. Но опять же, это требует усилий от людей, не являющихся техническими специалистами, для правильного обновления json и поддержания структуры, и по мере того, как json становится большим, его становится сложно поддерживать, это было не очень простым решением для нетехнических людей.
  3. CMS: Другим доступным вариантом было использование нашей системы управления контентом. Это обеспечивает преимущества пользовательского интерфейса и поддерживает структуру данных JSON самостоятельно, а не технические специалисты могут легко обновлять данные, не заботясь о структуре данных, и могут легко управлять конфигурациями. И это устраняет риск обновления БД и упрощает обслуживание благодаря преимуществам пользовательского интерфейса. Поэтому мы решили выбрать CMS для требований к данным объявлений.

Бизнес-логика:

Существует 2–3 различных фактора, на основе которых мы решаем, показывать ли объявления, и если да, то какое объявление показывать, если их несколько. Обратите внимание, что текущая реализация пользовательского интерфейса на внешнем интерфейсе обрабатывает только одно объявление, поэтому одновременно может отображаться только одно объявление.

  • Основным решающим фактором является то, установлена ​​ли видимость объявлений как истинная или нет.
  • В объекте данных объявлений, если ключ visible установлен как true, это означает, что объявление должно быть видимым.
  • Если одно объявление относится к конкретному брокеру, а одно объявление относится к типу общих объявлений, то объявление, относящееся к конкретному брокеру, имеет более высокий приоритет и отображается пользователю в первую очередь.
  • Если несколько объявлений одного типа отображаются как истинные, мы показываем объявление, которое идет первым в массиве объявлений.

Техническая реализация

В целом, это то, что нам нужно учитывать при реализации:

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

Получение и преобразование объявлений:

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

Самый простой способ получить эти объявления — создать компонент Announcement, получить необходимые данные при монтировании, преобразовать их по мере необходимости и соответствующим образом отобразить пользовательский интерфейс компонента, и все это внутри компонента.

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

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

Вот как работает окончательная настройка для выборки и преобразования данных:

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

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

Здесь мы делаем два вызова API: один для получения общих объявлений, а другой для получения объявлений, специфичных для брокера. Поскольку мы не хотим влиять на наш TTL и производительность, и мы хотели, чтобы они оба были доступны быстро, мы использовали здесь Promise.all для получения объявлений. Мы реализовали это таким образом, что если одно обещание терпит неудачу, мы гарантируем, что все обещание не будет отклонено, вместо этого, если одно обещание терпит неудачу, оно должно получить другое и сохранить данные в общем состоянии.

export const BlockingAnnouncementContext = React.createContext();
// This is done to get better debugging experience in dev tools
BlockingAnnouncementContext.displayName = 'BlockingAnnouncementContext';
function commonAnnouncementPromise() {
  return fetchHandler('GET', CMS_URL + apiMap.COMMON_BLOCKING_ANNOUNCEMENTS)
    .catch((err) => {
      captureException(err, {
        level: Severity.ERROR,
      });
			// don't throw as we do not want the promise to be rejected
      return { hasError: true, err, data: [] };
    });
}
function brokerSpecificAnnouncementPromise() {
  return fetchHandler('GET', CMS_URL + apiMap.BROKER_SPECIFIC_BLOCKING_ANNOUNCEMENT)
    .catch((err) => {
      captureException(err, {
        level: Severity.ERROR,
      });
return { hasError: true, err, data: [] };
    });
}
export function BlockingAnnouncementProvider(props) {
  const [announcements, dispatch] = React.useReducer(
    blockingAnnouncementReducer, blockingAnnouncementInitialState,
  );
React.useEffect(() => {
    Promise.all([commonAnnouncementPromise(), brokerSpecificAnnouncementPromise()])
      .then(response => response.map(eachResponse => eachResponse.data))
      .then(([commonAnnouncements, brokerSpecificAnnouncements]) => {
        if (commonAnnouncements.hasError && brokerSpecificAnnouncements.hasError) {
          throw Error([commonAnnouncements.err, brokerSpecificAnnouncements.err]);
        } else {
          dispatch({
            type: actionsMap.SUCCESS,
            payload: { data: { commonAnnouncements, brokerSpecificAnnouncements } },
          });
        }
      })
      .catch((err) => {
        captureException(err, {
          level: Severity.ERROR,
        });
        dispatch({ type: actionsMap.ERROR });
      });
  }, []);
return (
    <BlockingAnnouncementContext.Provider
      value={announcements}
    >
      { props.children }
    </BlockingAnnouncementContext.Provider>
  );
}
BlockingAnnouncementProvider.propTypes = {
  /**
   * to render childrens with blocking announcement context
   */
  children: PropTypes.node.isRequired,
};

Вот как работает крючок

import * as React from 'react';
import { BlockingAnnouncementContext } from '~/context/BlockingAnnouncementContext';
import { announcementsReducer, initialState } from './utils';
import { actionsMap } from './constants';
function findFirstVisibleAnnouncement(announcementsArray = []) {
  const announcement = announcementsArray.find(
    eachAnnouncement => eachAnnouncement.visible,
  ) || null;
  return announcement;
}
/**
 * custom hook to fetch the data from BlockingAnnouncementContext and find out
 * the announcement which should be visible to user according to priority and
 * availability.
 *
 * Note :- Broker Specific announcements have more priority over common announcements
 * and hence will be visible to user first when both type of announcements are visible.
 */
export default function useBlockingAnnouncement() {
  const { loading, error, data } = React.useContext(BlockingAnnouncementContext);
const [announcementsData, setAnnouncementsData] = React.useReducer(
    announcementsReducer, initialState,
  );
const getAnnouncement = React.useCallback(() => {
    const announcementsArray = [...data.brokerSpecificAnnouncements, ...data.commonAnnouncements];
    const announcement = findFirstVisibleAnnouncement(announcementsArray);
    return announcement;
  }, [data]);
React.useEffect(() => {
    if (!loading && !error) {
      const announcement = getAnnouncement();
      setAnnouncementsData({ type: actionsMap.SUCCESS, payload: { announcement } });
    } else if (!loading && error) {
      setAnnouncementsData({ type: actionsMap.ERROR });
    }
  }, [loading, error, getAnnouncement]);
return announcementsData;
}

Разработка пользовательского интерфейса

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

Проблема 1:

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

Решение:

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

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

import * as React from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router';
import useBlockingAnnouncement from '~/hooks/useBlockingAnnouncements';
import { setOverrideDefaultPageMargin } from '~/root-store/actions/uiFlagsActions';
import {
  BlockingAnnouncementUi,
  BlockingAnnouncementErrorBoundary,
} from '../BlockingAnnouncementComponents';
// Constants & utils
import {
  shouldShowHoldingsAuthBanner,
  shouldShowBlockingAnnouncement,
} from './utils';
const componentMap = {
  LOADING: 'LOADING',
  ERROR: 'ERROR',
  BLOCKING_ANNOUNCEMENT_UI: 'BlockingAnnouncementUI',
  HOLDINGS_AUTH_BANNER: 'HoldingsAuthBanner',
};
/**
 * helper function to return the component which needs to be re-render
 * for blocking Announcements.
 *
 * @param {boolean} loading - tells whether the data is fetched or being fetched
 * @param {boolean} error - denotes the whether an error as happened during fetch or not.
 * @param {( import('~/types/announcementsType').BlockingAnnouncement | null )} announcement -
 * announcement object to detect if announcement is available
 * or not.
 * @param {string} pathname - pathname of the current page to check whether to show holdings
 * auth banner or not
 * @returns {String} type of the component to render
 */
function getComponentToRender(loading, error, announcement, pathName) {
  if (loading) {
    return componentMap.LOADING;
  }
  if (error) {
    return componentMap.ERROR;
  }
  if (shouldShowBlockingAnnouncement(announcement, pathName)) {
    return componentMap.BLOCKING_ANNOUNCEMENT_UI;
  }
  ...
  //show other components
  ...
  ...
  return null;
}
/**
 * SceneHeaderManager component for deciding which component needs to be rendered
 * for scene header either to render the blocking announcement or holdings auth banner.
 *
 * @returns {(JSX.Element | null)} returns the component to render or null
 * if no component needs to be rendered.
 */
function SceneHeaderManager() {
  const [component, setComponent] = React.useState(null);
  const { loading, error, announcement } = useBlockingAnnouncement();
  const dispatch = useDispatch();
  const location = useLocation();
React.useEffect(() => {
    const componentToRender = getComponentToRender(
      loading,
      error,
      announcement,
      location.pathname,
    );
    if (
      componentToRender === componentMap.BLOCKING_ANNOUNCEMENT_UI
    ) {
      dispatch(setOverrideDefaultPageMargin(true));
      setComponent(componentToRender);
    } else {
      dispatch(setOverrideDefaultPageMargin(false));
      setComponent(componentToRender);
    }
  }, [loading, error, announcement, dispatch, location.pathname]);
switch (component) {
    case componentMap.BLOCKING_ANNOUNCEMENT_UI:
      return (
        <BlockingAnnouncementErrorBoundary>
          <BlockingAnnouncementUi
            title={announcement.title}
            description={announcement.description}
            src={announcement.src?.url}
            primaryCta={announcement.primaryCta}
            secondaryCta={announcement.secondaryCta}
          />
        </BlockingAnnouncementErrorBoundary>
      );
			...
			...
     default:
      return null;
  }
}
export default SceneHeaderManager;

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

  • Визуализируйте этот компонент на каждой странице. Хотя это было так же просто, как делать <SceneHeaderManager /> вверху каждой страницы, это было неэффективно, так как нам нужно было бы не забывать добавлять это с каждой новой страницей, которую мы создаем.
  • Другой способ заключался в том, чтобы интегрировать его на уровне корня/маршрутизатора, чтобы он отображался перед страницей в DOM. Это имеет больше смысла семантически, поскольку объявления являются глобальными, и отдельные страницы не должны заботиться о них.

Мы выбрали второй подход и интегрировали его на корневом уровне наших сцен, который, конечно же, был нашим маршрутизатором.

Но была еще большая проблема.

Проблема: проблемы с маржей

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

Поскольку этот компонент был обработан на уровне маршрутизатора, вне отдельных компонентов страницы, он каким-то образом должен позаботиться о необходимом поле на основе фиксированного заголовка. И если он отображается, то каждая страница, которая по умолчанию имеет верхнее поле из-за глобального класса, не должна применять свое поле по умолчанию ≥56px, вместо этого они должны применять только поле по отношению к этому компоненту объявления.

Вот как в целом выглядят страницы, с их полем по умолчанию, с учетом фиксированного заголовка.

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

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

Чтобы решить эту проблему, мы разбиваем задачу на две части;

  • [Задача 3a], чтобы исправить отступ между заголовком и баннером и
  • [Проблема 3b], чтобы исправить отступ между баннером и страницей. Потому что мы хотим, чтобы баннер объявлений о блокировке выглядел как часть страницы.

Возможный подход 1

Удалите абсолютное позиционирование заголовка, удалите поля по умолчанию со всех страниц и добавьте немного нижнего поля к заголовку. Это гарантирует, что все, что идет после заголовка в DOM, будет правильно размещено из-за нижнего поля заголовка. Чтобы убедиться, что заголовок остается липким, мы могли бы использовать position: sticky. Но с этим были некоторые проблемы.

  1. Позиция sticky поддерживается не всеми браузерами.
  2. Некоторым страницам вообще не нужны поля, и они должны перекрывать заголовок для отображения своего конкретного пользовательского интерфейса. Если мы сделаем заголовок статическим, то нам нужно будет сделать эти заголовки, специфичные для страницы, абсолютными, чтобы они могли перекрывать заголовок. Это не плохо, но не так эффективно.

Возможный подход 2

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

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

Возможный подход 3:

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

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

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

Решение 3а. Отступ между заголовком и объявлением

SceneHeaderManager знает, видимо объявление или нет, и просто устанавливает флаг в избыточности, когда объявление видно. Этот флаг может использоваться любым другим компонентом, чтобы знать, что объявления видны.

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

import { useSelector } from 'react-redux';
import SceneHeaderManager from '../SceneHeaderManager';
import './SceneHeader.css';
/**
 * Header Wrapper Component for blocking announcement. It is used to display
 * the announcements on header with correct margin or no margin when no announcement is visible.
 */
export default function SceneHeader() {
  const { overrideDefaultPageMargin } = useSelector(state => state.uiFlags);
return (
    <div styleName={`${overrideDefaultPageMargin ? 'header-wrapper' : ''}`}>
      <SceneHeaderManager />
    </div>
  );
}

Решение 3b: отступ между объявлением и контейнером страницы

Мы создали компонент макета, который будет контейнером макета по умолчанию для каждой страницы. Каждая страница может передать поле, которое она хочет иметь от заголовка, в качестве реквизита для этого компонента, при условии, что объявление не видно. Этот компонент-оболочка макета подпишется на redux, чтобы понять, видимо ли объявление, и в этом случае он просто отобразит margin-top: 0, поскольку отступ между объявлением и контейнером страницы будет контролироваться margin-bottom объявления, и если объявление не видно, оно будет отображать поля, пройденные страницей. Это требует, чтобы мы заменили компонент-контейнер каждой страницы этой оболочкой макета. Это был приемлемый компромисс, даже несмотря на то, что у него та же проблема, что и для любой страницы, которую мы добавим в будущем, необходимо убедиться, что она использует эту оболочку макета в качестве своего компонента-контейнера.

...
...
/**
 * Layout wrapper component for scenes. This component is responsible
 * for overriding the default page container classes if 
 * an announcement is rendered between the header and the page.
 *
 */
function SceneWrapper(props) {
  const { overrideDefaultPageMargin } = useSelector(state => state.uiFlags);
return (
    <div
      styleName={`${overrideDefaultPageMargin ? 'override-page-margin' : ''}`}
      className={props.className}
    >
      {props.children}
    </div>
  );
}
export default SceneWrapper;

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

Обработка ошибок

  • Если вызов API терпит неудачу, мы регистрируем ошибку в нашей системе отслеживания ошибок, и компонент не отображается. Ничего не ломается.
  • Поскольку эти данные поступают от cms во время выполнения, существует небольшая вероятность того, что данные могут быть получены в неправильном формате, если схема по ошибке настроена неправильно. Если такое произойдет, это сломает все наше приложение, потому что этот компонент отображается на уровне маршрутизатора. Было бы нецелесообразно проверять всю схему на fronted, и мы не хотели бы отображать наполовину сломанный пользовательский интерфейс для объявления, если в ответе API отсутствует какое-то обязательное поле. Поэтому мы добавили уровень компонента ErrorBoundary для обработки таких ошибок всякий раз, когда они происходят, и предотвращения сбоя всего приложения. Граница ошибки проста и ничего не отображает, если что-то ломается, и просто регистрирует ошибку в нашем механизме отслеживания ошибок.

Уроки из прошлого опыта с конфигами

Проблема с кэшированием

  • Как уже обсуждалось выше, этот конфиг должен был контролироваться продуктом во время выполнения, поэтому мы решили использовать для этого нашу существующую настройку CMS (strapi). Браузер делает вызов API к CMS, чтобы получить эти данные во время выполнения. Браузер будет эвристически кэшировать ответ, если мы не применим к ответу какие-либо заголовки управления кешем.
  • В прошлом мы сталкивались с этой проблемой с другими конфигурациями, которые мы используем на платформе, где конфигурации были кэшированы браузером, а затем всякий раз, когда мы их обновляем, это не отражается на конечном пользователе. Мы хотели избежать этой проблемы с кешированием браузера, чтобы пользователю не приходилось без необходимости видеть объявление, даже если оно было удалено.
  • Мы начали искать возможные решения этой проблемы. Поскольку наша cms размещена у нас, мы хотели, чтобы сервер cms фактически добавлял эти заголовки, но это было неудобно, так как код CMS приходилось изменять, чтобы приспособиться к этому, каждый раз, когда добавлялся новый вызов API, и это было непросто. способ централизовать эту обработку в CMS. Поэтому мы решили справиться с этим на уровне CDN, который находится поверх CMS. CMS находится за облачным фронтом, чтобы предотвратить дополнительную нагрузку на сервер.
  • Наконец, мы добавили лямбду на краю, которая добавляет эти заголовки к ответу нашего сервера, отправленному сервером cms, и эти ответы кэшируются на облачном фронте, и всякий раз, когда запрос от внешнего интерфейса попадает на облачный фронт, он передает эти заголовки в браузер и браузер не кэширует эти вызовы API.
  • Примечание. Наша лямбда-выражение на границе настраивается в ответе сервера, что означает, что всякий раз, когда запрос от облачного фронта отправляется на сервер, и когда сервер отвечает, наша лямбда-выражение изменяет этот ответ и применяет заголовки управления кешем к этому ответу и передает его в облачный фронт. , поэтому лямбда оптимизирована для запуска только при необходимости.

Проблемы с текущей реализацией и планы на будущее

  • У нас уже есть структура объявлений, которая заботится о глобальных объявлениях/уровня страниц/уровня компонентов. Текущая реализация блокировки объявлений не соответствует существующей структуре, поскольку соображения при создании этих объявлений сильно отличались от нашей существующей структуры, мы пытаемся выяснить, как сделать эти объявления частью структуры.
  • Вся настройка CMS находится за Cloudfront, и всякий раз, когда в существующих объявлениях происходят изменения, мы должны вручную аннулировать кеш cloudfront, чтобы обновленная конфигурация могла быть получена платформой и предоставлена ​​пользователям. Нам нужно автоматизировать это.
  • Нет простого способа настроить заголовки управления кешем для ответа CMS. Таким образом, у нас есть целая лямбда в настройке Edge, которая может добавлять заголовки кеша, чтобы мы не сталкивались с проблемами с кэшированием браузера по умолчанию. Но это дополнительная вещь, с которой мы должны справиться, и если мы изменим нашу настройку CDN, нам также придется найти какую-то альтернативу этой настройке.
  • Некоторым образом автоматизируйте настройку, чтобы не требовалось ручное вмешательство для включения объявления, аннулирования кеша и т. д.
  • Узнайте, как мы можем ускорить ручной процесс, если мы не можем его автоматизировать
  • При необходимости изучите другие способы информирования пользователей (электронная почта, WhatsApp, уведомления приложений).