功能开关不只是控制一个按钮是否显示,它把发布动作从部署动作中剥离出来,让团队可以在生产环境按用户、组织、地域或百分比逐步放量。Vue 3 项目如果一直在 .env 文件或本地常量里维护开关,很快就会遇到两个问题:开关粒度太粗,不能针对特定用户群;任何调整都需要重新构建和发布。LaunchDarkly 提供托管式的标志评估与流式更新能力,配合 Vue 3 的响应式 API 可以把远端标志变化实时映射到组件状态中。本文将完整介绍初始化、响应式桥接、组合式函数封装、多环境切换以及本地缓存降级。

下面先从 SDK 的接入方式开始。要理解的关键点是,LaunchDarkly 客户端 SDK 维护着一个独立于 Vue 响应式系统的内部状态,只有在它触发事件时手动同步到 reactive 对象,组件才能获得响应式更新。
一、初始化 LaunchDarkly 并接入 Vue 3 响应式系统
在 Vue 3 项目中安装官方浏览器 SDK 后,首先需要创建一个客户端实例。LaunchDarkly 的 JavaScript SDK 默认使用流式连接接收标志变更,相比传统轮询方式延迟更低,也不会因为定时器频繁请求造成不必要的网络开销。初始化的核心参数是客户端侧 ID(client-side ID)和用户上下文对象。用户上下文用于规则匹配,例如给灰度测试组用户返回 true,其他用户返回 false。
初始化代码通常会放在应用入口文件中,因为只有等 SDK 完成首次评估后,页面才能拿到可靠的标志值。下面的代码创建了一个全局响应式对象 featureFlags,并把 SDK 返回的全部标志写入其中。随后监听 change 事件,这样当运营人员在 LaunchDarkly 后台调整规则或开关时,Vue 组件可以立即感知到变化。
import { reactive } from 'vue';
import { initialize } from 'launchdarkly-js-client-sdk';
export const featureFlags = reactive({});
let ldClient = null;
export async function initFeatureFlags(userContext) {
const context = {
kind: 'user',
key: userContext.key,
email: userContext.email,
groups: userContext.groups
};
ldClient = initialize(import.meta.env.VITE_LD_CLIENT_SIDE_ID, context);
await ldClient.waitForInitialization();
const allFlags = ldClient.allFlags();
Object.keys(allFlags).forEach((key) => {
featureFlags[key] = allFlags[key];
});
ldClient.on('change', (changes) => {
Object.keys(changes).forEach((key) => {
featureFlags[key] = changes[key].current;
});
});
return ldClient;
}
这里必须等待 waitForInitialization(),否则首次渲染时 allFlags() 可能返回空对象,导致功能默认值误判。同步逻辑写在一个独立的 store 文件中,可以避免 main.ts 过于臃肿,也方便后面组合式函数引用。组件里不要直接调用 SDK 的 variation() 方法,因为它返回的是普通值,不具备 Vue 响应式能力,必须通过上面这层响应式对象桥接。
在组件中消费时,建议用计算属性读取 featureFlags 中的键。例如:
import { computed } from 'vue';
import { featureFlags } from '@/stores/featureFlagStore';
const isNewCheckout = computed(() => featureFlags['new-checkout'] === true);
这样模板中就可以使用 <template v-if="isNewCheckout"> 和 <template v-else> 切换不同组件。需要注意,如果标志值本身是对象或数组,computed 中要避免直接修改它,否则会绕过 LaunchDarkly 的规则评估结果。
二、封装可复用的 useFeatureFlag 组合式函数
直接暴露 featureFlags 给所有组件虽然简单,但会带来两个维护问题:一是每个使用点都要重复写默认值判断,二是键名散落在业务代码中容易拼写错误。在 Vue 3 中更适合用组合式函数统一封装访问逻辑。这样组件只依赖一组语义明确的函数,不关心底层状态来自哪里。
一个基础的 useFeatureFlag 实现如下。它接收标志键和默认值,返回一个只读计算属性。当远端值尚未到达或键不存在时,计算属性退回默认值,避免页面因 undefined 而报错。
import { computed } from 'vue';
import { featureFlags } from './featureFlagStore';
export function useFeatureFlag(key, defaultValue = false) {
return computed(() => {
const value = featureFlags[key];
return value === undefined ? defaultValue : value;
});
}
export function useBooleanFlag(key, defaultValue = false) {
return useFeatureFlag(key, defaultValue);
}
对于布尔型标志,可以进一步提供 useBooleanFlag,让调用处更直观。如果项目使用 TypeScript,还可以把标志键定义成常量对象或枚举,在组合式函数签名中限制 key 的可选范围。例如:
export const FLAG_KEYS = {
newCheckout: 'new-checkout',
aiSearch: 'ai-search',
payLater: 'show-pay-later'
} as const;
export function useFeatureFlag(key: keyof typeof FLAG_KEYS, defaultValue: boolean) {
return computed(() => {
const value = featureFlags[FLAG_KEYS[key]];
return value === undefined ? defaultValue : value;
});
}
需要注意的是,组合式函数只有在 initFeatureFlags() 完成之后再被调用才有意义。对于根组件之外的路由懒加载页面,可以在路由守卫中等待客户端 ready,或者在应用挂载前先显示加载状态。最稳妥的方式是在 main.ts 中先 await initFeatureFlags() 再执行 app.mount('#app'),这样所有组件渲染时标志已经就绪。
除了布尔值,LaunchDarkly 还支持 JSON 变体,例如返回配置对象、字符串模板或数字阈值。封装时不要把所有标志都压平成布尔值,应该保留原始类型,通过泛型或显式类型判断在调用处决定业务逻辑。比如某个标志的值是一个数组,就可以用 useFeatureFlag<string[]> 的方式读取,避免后续类型断言到处出现。
三、多环境配置与用户上下文设计
不同环境必须使用不同的客户端侧 ID,并且应将 ID 放在环境变量中而不是硬编码。Vite 项目中使用 import.meta.env.VITE_LD_CLIENT_SIDE_ID,CI 构建时根据环境注入对应值。不要把生产环境的 ID 提交到仓库,也不要让测试环境复用生产标志。LaunchDarkly 中的环境(Environment)概念与项目(Project)是分离的,同一个标志键可以在不同环境配置不同的默认规则。
用户上下文是标志精准投放的关键。不要只传一个 userId,最好把稳定属性一次性补齐,包括 email、groups、organization、device、country 等。这些属性在后台可以用于规则匹配,例如让 groups 包含 vip 的用户优先看到新功能。如果用户登录状态变化,需要调用 identify() 更新上下文,并且重新触发评估。下面是一个更新上下文的示例:
async function updateUserContext(user) {
const context = {
kind: 'user',
key: user.id,
email: user.email,
groups: user.role === 'vip' ? ['vip', 'beta'] : ['standard'],
custom: {
plan: user.plan,
region: user.region
}
};
await ldClient.identify(context);
const allFlags = ldClient.allFlags();
Object.keys(allFlags).forEach((key) => {
featureFlags[key] = allFlags[key];
});
}
这里 identify() 会重新建立连接并重新评估当前用户的所有标志,适合登录、切换组织或权限变化时调用。更新后必须再次把全量标志写回 featureFlags,否则 Vue 组件中的计算属性不会收到新值。对于匿名访问的首次渲染,可以先用一个临时 key,等登录完成后再 identify,同时保留旧标志作为降级值。
灰度发布通常使用百分比展示位或目标规则。LaunchDarkly 后台可以按属性组合、时间窗和流量比例配置,Vue 端不需要写额外的灰度逻辑,只需要传入准确的上下文。这样业务代码只依赖标志结果,发布策略完全由后台控制。比如新结账流程先对 5% 的 vip 用户开放,前端无需改代码即可调整比例。
四、本地缓存降级与客户端安全边界
尽管 SDK 具备流式更新能力,但网络抖动或 LaunchDarkly 服务不可用时,页面仍需要一个可用的初始状态。可以在初始化前从 localStorage 读取上一次缓存的标志值,先填充 featureFlags,等 SDK ready 后再覆盖为最新值。这样用户刷新页面时不会出现功能闪变或空白区域。
const FLAG_CACHE_KEY = 'ld-feature-flags';
function loadCachedFlags() {
const cached = localStorage.getItem(FLAG_CACHE_KEY);
if (!cached) return;
try {
const parsed = JSON.parse(cached);
Object.keys(parsed).forEach((key) => {
featureFlags[key] = parsed[key];
});
} catch (error) {
localStorage.removeItem(FLAG_CACHE_KEY);
}
}
ldClient.on('change', () => {
localStorage.setItem(FLAG_CACHE_KEY, JSON.stringify(featureFlags));
});
在执行 initFeatureFlags() 之前调用 loadCachedFlags(),可以让组件首屏使用上一次的开关状态,待远端标志返回后再自动更新。需要注意缓存只用于 UI 降级,不能把缓存结果作为权限判断的唯一依据。客户端侧标志本来就不适合保护敏感操作或数据接口,因为用户可以通过浏览器开发者工具修改内存或本地存储。
安全边界是很容易被忽略的问题。LaunchDarkly 客户端 SDK 适用于前端 UI 级别的功能切换,真正的权限校验必须由后端再次执行。比如前端隐藏了导出按钮,并不代表用户不能直接调用导出接口。对于涉及数据访问、支付或角色能力的场景,应当由服务端 SDK 评估同一标志或独立权限规则,浏览器端只负责展示层控制。
性能方面,不要为了兼容旧代码再额外启动 setInterval 轮询标志。SDK 默认的流式连接已经保证变更能推送到客户端,重复轮询不仅增加服务端压力,也容易和 Vue 的响应式更新互相干扰。如果网络环境确实无法使用流式连接,可以在初始化时通过 options 指定轮询模式,但要设置合理的间隔并只在必要时使用。
Vue 3Feature FlagLaunchDarkly修改时间:2026-10-05 06:06:44