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

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

Переключение feature flag

В современной разработке программного обеспечения 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 в современных приложениях. Но приведенная выше реализация должна быть достаточно удовлетворительной для начала.

Ссылка на полное руководство по настройке на GitHub.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Eshank Vaish: How to Implement Feature Gates in React?

Предыдущая статья5 удивительных скрытых возможностей Python. Часть 2
Следующая статьяКак объединить мобильные сервисы Google и Huawei в одной кодовой базе