Как реализовать feature gate в React

Как реализовать feature gate в React

https://t.me/ai_machinelearning_big_data

Feature gate — это высокоуровневый инструмент, позволяющий командам настраивать доступные пользователю функции без обновления кода приложения.

В современной разработке программного обеспечения feature gate, часто называемый feature toggle или feature flag, является важным инструментом для управления релизом новых функций. Посмотрим, как реализовать feature gate во время сборки в React-приложении.

Что такое feature gate?

Feature gate, также известный как feature toggle или feature flag, — это метод разработки программного обеспечения, позволяющий командам контролировать доступность определенных функций и возможностей в приложении без явного внесения изменений в код. Он предоставляет альтернативу поддержанию нескольких функциональных ветвей в исходном коде, поскольку разрабатываемый код или функция могут быть объединены с основной ветвью путем отключения соответствующего feature gate.

Настройка React-проекта

Для реализации feature gate в приложении будем использовать React и TypeScript с шаблоном ESLint для настройки шаблонного кода. Если у вас уже есть разработанное ранее React-приложение, шаблон можно не использовать.

Реализация feature gate

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

Конфигурация feature gate

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

const featureFlags = {
  featureA: true,
  featureB: false,
  featureC: true,
};

Но данная конфигурация должна быть настраиваемой без необходимости изменения кода, верно? Одним из способов достижения этой цели является определение файла констант featureFlag.ts, который будет считывать конфигурацию из .env— или json-файла. Мы будем использовать .env-файл для управления флагами функций, а в дальнейшей части статьи рассмотрим, как получить конфигурацию из API и сделать ее динамической в подлинном смысле этого слова.

Сначала определим .env-файл. Поскольку Vite автоматически подставляет переменные с префиксом VITE_ из .env-файла, у нас будет две переменные: VITE_ALLOWED_FLAGS и VITE_BLOCKED_FLAGS, каждая из которых принимает строку, разграниченную символом ,. Разрешенные флаги имеют значение true, а заблокированные —  false

VITE_ALLOWED_FLAGS=flagA,flagB
VITE_BLOCKED_FLAGS=flagX,flagY

Теперь, определив файл .env, посмотрим и на файл featureFlags.ts. Этот файл будет отвечать за парсинг обеих переменных окружения и преобразование их в объектную структуру, о которой шла речь ранее.

const { VITE_ALLOWED_FLAGS = '', VITE_BLOCKED_FLAGS = '' } = <
 {
  VITE_ALLOWED_FLAGS?: string;
  VITE_BLOCKED_FLAGS?: string;
 }
>import.meta.env;

/**
 * @param featureList Список признаков, разделенных символами ','
 * @param defaultValue Значение, присваиваемое флагам в списке флагов
 * @returns Объект Feature flag, содержащий в качестве ключа имя feature flag и 
 * булево значение.
 */
const getFeatureFlagsFromFeatureList = (
 featureList: string,
 defaultValue: boolean
) =>
 featureList.split(',').reduce((flags, flagName) => {
  const flag = flagName.trim();
  if (flag) {
   flags[flag] = defaultValue;
  }
  return flags;
 }, {} as Record<string, boolean>);

const allowedFlags = getFeatureFlagsFromFeatureList(VITE_ALLOWED_FLAGS, true);

const blockedFlags = getFeatureFlagsFromFeatureList(VITE_BLOCKED_FLAGS, false);

/**
 * Заблокированные флаги в случае конфликта будут переписывать разрешенные флаги.
 * Например, если разрешенные флаги - feat1,feat2, а заблокированные - feat2,feat3,
 * то в объекте featureFlags значение feat2 будет равно false.
 */
export const featureFlags = {
 ...allowedFlags,
 ...blockedFlags,
};

Провайдер feature flag

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

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

import { createContext, useContext } from 'react';

export type FeatureFlags = Record<string, boolean | undefined>;

export interface FeatureFlagProps extends React.PropsWithChildren {
 /**
  * Feature flags для приложения.
  */
 featureFlags: FeatureFlags;
}


/**
 * Контекст для хранения значений feature flags для приложения.
 */
const FeatureFlagContext = createContext<FeatureFlags>({});

/**
 * Провайдер для feature flags.
 * При необходимости можем добавить пользовательскую логику для флагов.
 */
export const FeatureFlagProvider = ({
 children,
 featureFlags = {},
}: FeatureFlagProps) => (
 <FeatureFlagContext.Provider value={featureFlags}>
  {children}
 </FeatureFlagContext.Provider>
);

/**
 *
 * @returns Feature flags доступны в контексте. 
 */
export const useFeatureFlags = () => {
 const featureFlags = useContext(FeatureFlagContext);

 return featureFlags;
};

Добавление провайдера в приложение

Теперь, когда у нас готовы конфигурация и провайдер, пришло время обернуть приложение провайдером, чтобы можно было получить доступ к флагам во всем приложении. Для этого необходимо обновить файл main.tsx, добавив провайдер вокруг компонента <App /> таким образом, чтобы он выглядел примерно так:

import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './App';
import { featureFlags } from './constants/featureFlags';
import { FeatureFlagProvider } from './providers/FeatureFlag';
import './index.css';

// Мы также можем применить бизнес-логику к значениям флагов
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
 <React.StrictMode>
  <FeatureFlagProvider featureFlags={featureFlags}>
   <App />
  </FeatureFlagProvider>
 </React.StrictMode>
);

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

Использование

Пришло время использовать feature flags в приложении для переключения между функциями. Попробуем применить flagA, определенный ранее в файле среды, в компоненте <App /> следующим образом для переключения текста на странице:

import { useState } from 'react';

// Обновляем импорт
import { useFeatureFlags } from './providers/FeatureFlag';
import reactLogo from './assets/react.svg';
import './App.css';

function App() {
 const [count, setCount] = useState(0);
 // Используем хук feature flag 
 const featureFlags = useFeatureFlags();
 const { flagA } = featureFlags;

 return (
  <div className="App">
   <div>
    <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
     <img src="/vite.svg" className="logo" alt="Vite logo" />
    </a>
    <a href="https://reactjs.org" target="_blank" rel="noreferrer">
     <img src={reactLogo} className="logo react" alt="React logo" />
    </a>
   </div>
   <h1>Vite + React</h1>
   <p>
    {flagA
     ? 'I would show up when flagA is truthy'
     : 'I would show up when flagA is not truthy'}
   </p>
   <div className="card">
    <button onClick={() => setCount((oldCount) => oldCount + 1)}>
     count is {count}
    </button>
    <p>
     Edit <code>src/App.tsx</code> and save to test HMR
    </p>
   </div>
   <p className="read-the-docs">
    Click on the Vite and React logos to learn more
   </p>
  </div>
 );
}

export default App;

Тестирование реализации

Наконец, настало время для тестирования. Посмотрим, как поведет себя реализация при переключении feature flag flagA в файле .env. Но перед этим необходимо запустить сервер разработки, выполнив следующую команду:

yarn dev

Сначала установим значение false с помощью следующей команды .env:

VITE_ALLOWED_FLAGS=flagB
VITE_BLOCKED_FLAGS=flagX,flagY

На приведенном ниже изображении можно заметить, что, как и ожидалось, появляется текст “I would show up when flagA is not truthy” (“Я бы появился, если бы flagA не был истинным”).

Результат при установке flagA в false

Включим flagA, обновив файл .env следующим образом:

VITE_ALLOWED_FLAGS=flagA,flagB
VITE_BLOCKED_FLAGS=flagX,flagY

Теперь на рисунке ниже мы видим то, что и ожидали: “I would show up when flagA is truthy” (“Я бы появился, если бы flagA был истинным”):

Результат при установке flagA в true

Бонус: интеграция с React Router

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

Для реализации логики фильтрации определения маршрутов можно использовать Guarded Route. Предоставление полного руководства выходит за рамки данной статьи, так что здесь приведен пример файла AppRoutes, который можно использовать в качестве справочника для реализации feature gates на маршрутах:

import { Route, Routes } from 'react-router-dom';
import GuardedRoute from './GuardedRoute';
import { useFeatureFlags } from './providers/FeatureFlag';

interface AppRoutesProp {
 /**
  * True, если пользователь аутентифицирован, false - в противном случае.
  */
 isAuthenticated: boolean;
}

const HOME_ROUTE = '/home';
const LOGIN_ROUTE = '/login';
const ABOUT_ROUTE = '/about';

const AppRoutes = (props: AppRoutesProp): JSX.Element => {
 const { isAuthenticated } = props;
 const { flagA, flagB } = useFeatureFlags();

 return (
  <Routes>
   {/* Unguarded Routes */}
   <Route path={ABOUT_ROUTE} element={<p>About Page</p>} />
   {/* Non-Authenticated Routes: accessible only if user in not authenticated */}
   <Route
    element={
     <GuardedRoute
      isRouteAccessible={!isAuthenticated && flagA}
      redirectRoute={HOME_ROUTE}
     />
    }
   >
    {/* Login Route */}
    <Route path={LOGIN_ROUTE} element={<p>Login Page</p>} />
   </Route>
   {/* Authenticated Routes */}
   <Route
    element={
     <GuardedRoute
      isRouteAccessible={isAuthenticated && flagB}
      redirectRoute={LOGIN_ROUTE}
     />
    }
   >
    <Route path={HOME_ROUTE} element={<p>Home Page</p>} />
   </Route>
   {/* Not found Route */}
   <Route path="*" element={<p>Page Not Found</p>} />
  </Routes>
 );
};

export default AppRoutes;

Как видите, мы закрыли маршруты login и home флагами flagA и flagB соответственно, поэтому если флаги установлены в false, то обращение к этим маршрутам приведет к результату 404. Обратите внимание на то, что аналогичную логику необходимо реализовать и для элементов, перенаправляющих на такие маршруты, таких как панель навигации.

Заключение

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


Источник

Report Page