技能 编程开发 功能开关实现与管理

功能开关实现与管理

v20260825
webiny-add-feature-flag
本技能指南详细介绍了在Webiny系统中添加、管理和控制功能开关(Feature Flag)。涵盖了从DTO类型定义、Zod模式验证到API级别、配置层和后台UI的开关控制,支持简单布尔值和复杂的嵌套分组,确保功能发布和控制的健壮性。
获取技能
361 次下载
概览

Adding a New Feature Flag

A WCP license is required for feature flags to work. The license is the gate; the config is the switch within the gate.

Decision Flow

1. No license at all             → false (everything off, config ignored)
2. License blocks the flag       → false (config ignored)
3. License allows + config=false → false (config can disable what license allows)
4. License allows + config=true  → true
5. License allows + config unset → true  (license is the authority for unset flags)
6. Not in LICENSE_CHECKS + license exists + config unset → true
7. Not in LICENSE_CHECKS + license exists + config=false → false

Key points:

  • Config can disable what the license allows, but cannot enable what the license blocks.
  • Flags not governed by a license (LICENSE_CHECKS) still require a license to exist — then config decides.
  • Without any license, all flags are off regardless of config.

Architecture

  • FeatureFlags class (packages/feature-flags/src/FeatureFlags.ts) — single isEnabled(name) method resolves dot-path strings against the DTO. Flags are disabled by default (undefined → false). Also provides isExplicitlyDisabled(name) to distinguish "not set" from "set to false".
  • IFeatureFlagsDto (packages/feature-flags/src/types.ts) — the typed DTO interface.
  • KnownFeatureFlag (packages/feature-flags/src/FeatureFlags.ts) — string literal union for autocomplete.
  • Zod schema (packages/project/src/extensions/FeatureFlags.tsx) — validates the config input.
  • toDto() returns the fully resolved state (all flags explicitly set), used by the featureFlags GraphQL query.
  • License decorators intercept isEnabled() and apply the decision flow above via a LICENSE_CHECKS map.

Steps to Add a Simple Boolean Flag

1. Add to DTO type

File: packages/feature-flags/src/types.ts

Add the new flag to IFeatureFlagsDto:

export interface IFeatureFlagsDto {
  // ... existing flags
  myNewFeature?: boolean;
}

2. Add to KnownFeatureFlag union

File: packages/feature-flags/src/FeatureFlags.ts

Add the string to the KnownFeatureFlag type:

export type KnownFeatureFlag =
  // ... existing flags
  "myNewFeature";

3. Add to toDto()

File: packages/feature-flags/src/FeatureFlags.ts

Add the flag to the toDto() method so the API returns it:

toDto() {
    return {
        // ... existing flags
        myNewFeature: this.isEnabled("myNewFeature")
    };
}

4. Add to Zod schema

File: packages/project/src/extensions/FeatureFlags.tsx

Add to the paramsSchema so users get validation in webiny.config.tsx:

myNewFeature: z.boolean().optional();

5. Gate the feature

At the config level (controls whether extensions mount at build time):

// In the extension component (e.g., MyFeature.tsx)
import { FeatureFlag } from "@webiny/project";

export const MyFeature = () => (
    <FeatureFlag.CanUse name="myNewFeature">
        <Api.Extension src={...} />
        <Admin.Extension src={...} />
    </FeatureFlag.CanUse>
);

Or add a named convenience component in packages/project/src/components/FeatureFlag.tsx:

function CanUseMyNewFeature({ children }: { children: React.ReactNode }) {
  return <CanUse name="myNewFeature">{children}</CanUse>;
}

At the admin runtime level (controls UI visibility):

import { useFeatureFlags } from "@webiny/app-admin";

const featureFlags = useFeatureFlags();
if (!featureFlags.isEnabled("myNewFeature")) {
  return null;
}

At the API runtime level (controls backend behavior):

import { FeatureFlags } from "~/features/featureFlags/abstractions.js";

// In a DI-resolved class:
constructor(private featureFlags: FeatureFlags.Interface) {}

someMethod() {
    if (!this.featureFlags.get().isEnabled("myNewFeature")) {
        return;
    }
}

6. User configuration

Users configure flags in webiny.config.tsx:

export const FeatureFlags = () => (
  <Project.FeatureFlags
    features={{
      myNewFeature: false // disabled
    }}
  />
);

Omitting a flag means the license decides (enabled if licensed, disabled if not). Setting a flag to false disables it even if the license allows it.

Adding a Nested Flag Group

For flags with sub-options (like aiPowerups or advancedAccessControlLayer):

DTO type — use a union:

export interface IMyFeatureOptions {
  subFeatureA?: boolean;
  subFeatureB?: boolean;
}

export interface IFeatureFlagsDto {
  myFeature?: boolean | IMyFeatureOptions;
}

KnownFeatureFlag — add parent and children:

export type KnownFeatureFlag = "myFeature" | "myFeature.subFeatureA" | "myFeature.subFeatureB";

toDto() — collapse parent when disabled:

myFeature: this.isEnabled("myFeature")
  ? {
      subFeatureA: this.isEnabled("myFeature.subFeatureA"),
      subFeatureB: this.isEnabled("myFeature.subFeatureB")
    }
  : false;

Zod schema — union type:

myFeature: z.union([
  z.boolean(),
  z.object({
    subFeatureA: z.boolean().optional(),
    subFeatureB: z.boolean().optional()
  })
]).optional();

User config:

// Disable entirely
<Project.FeatureFlags features={{ myFeature: false }} />

// Disable specific sub-feature
<Project.FeatureFlags features={{ myFeature: { subFeatureA: false } }} />

WCP License Gating

A WCP license is required for any feature flag to work. Without a license, all flags return false.

Flags NOT in LICENSE_CHECKS (like remoteComponents): a license must exist, but the license doesn't explicitly govern this flag. Config decides. Do NOT add a flag to LICENSE_CHECKS until the WCP backend supports it.

Flags IN LICENSE_CHECKS: the license explicitly gates the feature. If the license blocks it, the flag is false regardless of config. If the license allows it, config can still disable it.

To make a flag license-governed, add it to the LICENSE_CHECKS map in all three decorators:

  • API level: packages/api-core/src/features/featureFlags/decorators/FeatureFlagsWithLicenseDecorator.ts
  • Build level: packages/project/src/decorators/GetFeatureFlagsWithLicense.ts
  • Config level: packages/project/src/services/GetProjectConfigService/LicenseDecoratedFeatureFlags.ts
const LICENSE_CHECKS: Record<string, (license: ILicense) => boolean> = {
  // ... existing checks
  myNewFeature: l => l.canUseMyNewFeature()
};

This also requires adding canUseMyNewFeature() to the ILicense interface and its implementations in @webiny/wcp (License.ts, NullLicense.ts). Only do this when the WCP backend supports the flag.

Files Reference

Purpose File
DTO type packages/feature-flags/src/types.ts
FeatureFlags class + KnownFeatureFlag packages/feature-flags/src/FeatureFlags.ts
Zod schema packages/project/src/extensions/FeatureFlags.tsx
Config-level CanUse components packages/project/src/components/FeatureFlag.tsx
Admin hook packages/app-admin/src/presentation/featureFlags/useFeatureFlags.ts
API abstraction packages/api-core/src/features/featureFlags/abstractions.ts
API license decorator packages/api-core/src/features/featureFlags/decorators/FeatureFlagsWithLicenseDecorator.ts
Build license decorator packages/project/src/decorators/GetFeatureFlagsWithLicense.ts
Config license decorator packages/project/src/services/GetProjectConfigService/LicenseDecoratedFeatureFlags.ts
GraphQL query packages/api-core/src/graphql/featureFlags/FeatureFlagsSchemaFactory.ts
信息
Category 编程开发
Name webiny-add-feature-flag
版本 v20260825
大小 8.7KB
更新时间 2026-09-06
语言