在 Unleash Admin UI 中构建产品内 UX 调研组件UX Tweak Widgets 的架构、Flag 契约与实现剖析【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleashUnleash 前端在frontend/src/component/uxtweak/目录中实现了一套产品内in-appUX 调研组件用于在管理界面中展示由 UX Tweak 平台创建的问卷。本文以该目录的 ARCHITECTURE.md 为骨架结合源码深入讲解flag 即投放通道的契约设计、懒加载与错误隔离的组件流、问卷生命周期控制展示一次、7 天宽限期、3 次展示上限以及 fire-and-forget 的提交通道帮助你理解如何在 Unleash 管理后台中安全、可控地接入第三方产品内调研。概览为什么Flag 就是投放通道UX Tweak Widgets 是嵌入 Unleash Admin UI 的产品内 UX 调研组件目前仅支持问卷 survey。其运行模型与传统的前端打包问卷内容完全不同研究人员在UX Tweak平台编写问卷UX Tweak 将问卷发布为一个Unleash feature flag本目录通过 Admin UI 自己的前端 SDK clientunleash/proxy-client-react发现这个 flag并渲染对应 widgetAdmin UI自身不携带任何问卷内容——flag 的 variant payload 就是完整的问卷数据flag 即投放通道。也就是说问卷的创建、编辑、定向、灰度全在 Unleash 的 flag 机制上完成前端只负责发现并渲染。Flag 契约一个问卷 一个 feature flag命名约定一个问卷活动对应一个 feature flag命名必须遵循uxtweak-survey-page-slug-id只有uxtweak-survey-这个前缀是契约性的前缀之后的所有内容page-slug、id对消费者来说都是**不透明opaque**的不应被前端解析或依赖。源码中前缀常量定义在 surveys.tsexport const SURVEY_FLAG_PREFIX uxtweak-survey-;Variant 载荷完整的问卷 JSONflag 携带一个名为config的 variant其 JSON payload 就是整个问卷。契约示例如下{ v: 1, surveyId: sv_…, page: /projects, // 或 * 表示所有页面 title: Quick feedback, intro: …, questions: [ { id: q1, type: rating, prompt: …, required: true }, { id: q2, type: single, options: [a,b], prompt: …, required: false }, { id: q3, type: text, prompt: …, required: false } ], submitBase: https://… // 问卷响应将被 POST 到的 UX Tweak 服务端 }各字段语义对应 surveys.ts 的解析实现字段类型说明vnumberpayload 版本号当前必须为1否则整体拒绝surveyIdstring问卷全局唯一 id用于去重、宽限期与印象计数pagestring页面匹配规则具体路径如/projects或*所有页面titlestring卡片标题introstring可选引导文案非字符串时回退为空串questionsarray1~10 个问题对象MAX_QUESTIONS 10submitBasestringUX Tweak 服务端地址响应会 POST 到${submitBase}/public/survey/responses问题对象支持三种类型定义于SurveyQuestionConfigsurveys.tsrating星级评分idpromptrequired前端渲染为 MUIRating最大 5 星single单选必须提供非空字符串数组options前端渲染为 radio grouptext自由文本前端渲染为多行TextField。定向与决策边界定向逻辑不在这份代码里。投放百分比rollout percentage、约束条件constraints都定义在 flag 的 strategy 上由 Unleash 在客户端看到 flag之前完成评估。本目录从不做任何定向决策它只回答一个问题payload 里的page是否匹配用户当前正在看的页面匹配规则见pageMatchessurveys.tsexport const pageMatches (page: string, pathname: string): boolean page * || normalizePath(page) normalizePath(pathname);匹配是精确匹配但对结尾斜杠宽容normalizePath且*匹配一切。版本化与全有或全无解析payload 是版本化的v不是1时整份问卷被拒绝这样未来出现新形状的 payload 也绝不可能在旧消费者上半渲染。解析是全有或全无all-or-nothing的理由与版本化一致一份只能渲染一半的问卷比没有问卷更糟。因此只要有一个问题格式错误整个问卷就被拒绝。parseSurveyPayload对任何畸形输入都返回null而绝不抛出异常payload 类型不是 JSONJSON.parse失败非法 JSON解析结果不是对象注意null会被解析成null同样被拒缺少surveyId/page/title/submitBase等必填字段isNonEmptyString校验questions不是数组、为空或超过 10 个任一问题缺id/prompt、single类型没有非空字符串数组options、type不在三种类型之内。这样设计是因为扫描发生在 SDK 事件回调里、位于任何 React Error Boundary 之外抛异常会直接冒泡到外层应用。组件流门卫 → 懒加载 Runner → 问卷卡片整体组件层级如下App登录态分支App (logged-in branch) └─ UxTweakWidgets gate —— 主 bundle 中的唯一成员 └─ (lazy, error-isolated) UxTweakRunner 懒加载的 widget chunk每种 widget 对应一个宿主 └─ useActiveSurvey() → UxSurveyCardUxTweakWidgets门卫gateUxTweakWidgetsUxTweakWidgets.tsx是整个功能在主 bundle 中的唯一常驻代码。它挂在 App.tsx 中紧邻FeedbackNPS同时受两个条件门控{isLoggedIn uxTweakSurveysEnabled ? ( UxTweakWidgets /isLoggedIn绝不在登录页渲染uxTweakSurveys内部 uiConfig flagUNLEASH_EXPERIMENTAL_UX_TWEAK_SURVEYS企业版 uiConfig 标志见 uiConfig.ts同时充当总开关kill switch。门卫通过useFlags()监听 SDK client 是否有任何以uxtweak-开头的 flag只有发现时才lazy(() import(./UxTweakRunner.tsx))加载 widget chunk。没有任何活动campaign时UxTweakWidgets只产生一个事件订阅其余什么都不做——成本几乎为零。门卫使用useLatcheduseLatched.ts闩锁一旦某个uxtweak-flag 出现过就保持挂载。原因在于flag 刷新导致最后一个 flag 消失如 rollout 重新分桶、活动暂停时不能卸载 Runner 从而销毁用户正在作答的问卷。flag 消失之后已加载 chunk 渲染null是全部代价。子树自带静默 ErrorBoundaryfallbackRender{() null}没有它widget 崩溃会冒泡到ApplicationRoot中的应用级边界用错误布局替换整个 Admin UI。产品内调研绝不允许把产品搞挂。UxTweakRunner懒加载 chunk 与问卷宿主UxTweakRunnerUxTweakRunner.tsx是lazy()要求的默认导出模块。目前它同时兼任问卷宿主通过useLatched(useActiveSurvey())闩锁第一个由useActiveSurvey产出的问卷渲染该卡片直到访客完成作答提交或关闭卡片以key{survey.surveyId}标识跨 session 切换活动时不会残留组件状态。一旦显示卡片能扛过 flag 刷新、payload 编辑和路由变化——正在作答的访客绝不能被突然抽走卡片。由于闩锁保持了 config 的对象身份活动中的 payload 在线编辑不会重挂载 keyed 卡片也就不会清空用户已输入的答案。闩锁刻意永不清理作答完成后卡片自身的状态机渲染null宽限期又会压制其他问卷——因此一次会话最多展示一份问卷是结构上保证的by construction。当未来出现更多 widget 类型如 chat、interviews时每种类型会在这里拥有自己的宿主问卷专属的扫描逻辑届时再基于真实消费者做泛化而不是提前抽象。useActiveSurvey当前页面上的问卷useActiveSurveyuseActiveSurvey.ts是一个纯派生Hookexport const useActiveSurvey (): SurveyConfig | null { const flags useFlags(); const { pathname } useLocation(); if (isInSurveyGracePeriod()) { return null; } return ( scanSurveys(flags, pathname).find( (survey) !hasSeenSurvey(survey.surveyId) !hasReachedImpressionCap(survey.surveyId), ) ?? null ); };flag 变化时由 SDK 的useFlags()触发重渲染路由变化时由useLocation()触发——没有任何自定义订阅代码多个问卷匹配同一页面时flag 名最小者胜出——scanSurveys会sort因为 SDK 不保证多次刷新间 flag 的顺序赢家绝不能因页面加载而改变。survey/surveys.ts契约模块这是整个功能的契约中心surveys.ts集中了前缀常量SURVEY_FLAG_PREFIXpayload 类型定义SurveyConfig、SurveyQuestionConfig、SurveyAnswers全有或全无解析器parseSurveyPayload扫描管线scanSurveysflags → 前缀过滤 → 解析 → 页面匹配 → 排序。export const scanSurveys (flags: IToggle[], pathname: string): SurveyConfig[] flags .filter((flag) flag.name.startsWith(SURVEY_FLAG_PREFIX)) .map((flag) parseSurveyPayload(flag.name, flag.variant?.payload)) .filter((survey) survey ! null) .filter((survey) pageMatches(survey.page, pathname)) .sort((a, b) a.flagName.localeCompare(b.flagName));UxSurveyCard浮动的右下角问卷卡片UxSurveyCardUxSurveyCard.tsx是一个固定定位在右下角的浮动卡片position: fixed; bottom/right: theme.spacing(3)宽度 360px包含标题、intro、以表单呈现的问题、提交按钮。每个问题类型一个小组件rating→ MUIRating与FeedbackComponent一致最大 5 星single→ radio grouptext→ 多行TextField。所有问题都是受控输入共享一个以 question id 为 key 的answers记录。关键设计所有答案统一存为字符串评分也存字符串因此已作答只有一条规则——trim 后非空必答校验就是一次every()const canSubmit survey.questions.every( (question) !question.required || Boolean(answers[question.id]?.trim()), );提交按钮在必答题未全部作答前禁用点击提交后卡片切换到本地thanks 状态——居中的确认视图3 秒后自动淡出THANKS_VISIBLE_MS 3000。淡出调度是可注入的scheduleLeaveprop测试直接触发离开而不伪造 timer任何状态下都可以关闭。卡片状态机为answering | thanks | leaving | closedFade动画的onExited将状态置为closed完成卸载。提交fire-and-forget 的 POSTsubmitSurveyResponsesubmitSurveyResponse.ts将清理后的答案rating 转数字、空值剔除交给 Runner然后POST ${submitBase}/public/survey/responses { surveyId, visitorId, page, answers }UX Tweak 服务端按(survey, visitor)upsert因此没有 tokenvisitorId优先取 Unleash client 的sessionId即 rollout 粘性哈希所用的值缺失时铸造一个持久化的 UUIDuxtweak-visitor-id:v1crypto.randomUUID()POST 使用keepalive: true并在 Runner 中.catch(() {})吞掉失败——访客已经看到 thanks 视图产品内调研绝不能因为一次失败请求拖垮产品卡片保持纯展示presentationalI/O 归 Runner 所有。展示频率控制不打扰访客的三道闸一次会话最多一份问卷还不足以避免打扰模块在 localStorage 层实现了三道闸全部位于 seenSurveys.ts统一走仓库的createLocalStoragecreateLocalStorage.ts自动命名空间化、私密模式安全。1. 每个浏览器最多展示一次提交或关闭会把问卷 id 记入单个 localStorage 条目uxtweak-surveys-seen:v1字符串数组只保留最新 50 个id。useActiveSurvey每次扫描都用全新读取过滤已见 id因此已完结的问卷在路由变化、页面加载后都不会再现且无需任何响应式接线。因为每个 campaign 的surveyId全局唯一重新发布为新 campaign 会自然再次展示。2. 完结任何问卷触发 7 天全局宽限期markSurveySeen同时写入标记uxtweak-survey-grace:v1使用createLocalStorage自带的timeToLiveSEVEN_DAY_GRACE_PERIOD_MS 7 * 24 * 60 * 60 * 1000。useActiveSurvey在标记存在期间返回null存储层在读取时自动删除过期标记无需手写时钟数学。这正是让同时命中多个活动的访客不会在完成一份后立刻收到下一份的机制。宽限期逻辑内聚在markSurveySeen内意味着提交、关闭乃至未来的提交分支都会自动继承它。3. 被忽略的问卷在 3 次展示后停止出现Runner 每次页面加载为每份问卷记录一次印象uxtweak-survey-impressions:v1模块级Set让 remount 和 StrictMode 双重 effect 免费去重useActiveSurvey跳过已展示MAX_IMPRESSIONS 3次的问卷。被忽略的卡片不能永远纠缠用户但一眼瞥过也不该直接烧掉配额。与所有频率存储一样畸形条目 fail open问卷照常展示绝不会崩溃。值得了解的架构决策刻意使用 SDK 的useFlags()它包装了getAllToggles()返回的只是已为本访客评估为启用的 flag因此无需再检查enabled并且与isEnabled/getVariant不同不产生 impression 事件——发现逻辑绝不能污染分析数据。同时它拥有 update-event 订阅本目录因此没有任何订阅代码。用useLocation()匹配页面不做轮询BrowserRouter挂载时带basename因此 pathname 已排除应用的基础路径可以直接与 payload 的page比较。匹配精确、对结尾斜杠宽容*匹配一切。实际只有云版本生效SDK client 只有在服务端注入的unleashTokenmeta 标签存在时即 Unleash Cloud才会启动。自托管self-hosted安装时客户端处于惰性状态本目录渲染null。全程使用 MUI 主题令牌卡片遵循 Admin UI 主题含暗色模式与其它浮动组件如FeedbackNPS保持一致。路线图与现状模块的演进路线ARCHITECTURE.md✅ 发现 最小卡片标题/intro、仅会话内关闭✅ 问题渲染评分 / 单选 / 自由文本、必答校验、自动消失的 thanks 状态、最多展示一次抑制✅ 问卷间 7 天宽限期✅ 作答中闩锁卡片在 flag 刷新、payload 编辑、路由变化后仍存活直到完结✅ 印象上限被忽略的问卷 3 次展示后不再出现✅ 确定性问卷顺序每次页面加载 flag 名最小者胜出⏳ 进一步加固跨标签页同步cross-tab sync✅ 提交到submitBasefire-and-forget POSTvisitor id 取自 Unleash sessionId小结UX Tweak Widgets 是Unleash 能力自举的一个典型范例用 Unleash 自己的 flag、variant 和前端 SDK 来承载并定向产品内调研同时在前端用严格的契约解析、懒加载 错误隔离、闩锁语义和多层频率控制把第三方内容的风险压缩到最小。其设计原则——flag 即投放通道、前端不做定向决策、畸形输入永远返回 null 而非抛出、调研绝不能拖垮产品——对任何要在管理界面中集成第三方动态内容的场景都极具参考价值。若要在本地继续深入可从以下路径入手契约与解析surveys.ts、surveys.test.ts门卫与宿主UxTweakWidgets.tsx、UxTweakRunner.tsx、UxTweakWidgets.test.tsx频率控制seenSurveys.ts、seenSurveys.test.ts提交通道submitSurveyResponse.ts、submitSurveyResponse.test.ts卡片 UIUxSurveyCard.tsx、UxSurveyCard.test.tsx挂载位置与总开关App.tsx、App.test.tsx、uiConfig.ts【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考