frappe framework/ui 设计哲学以 FP1–FP3 生成式原则驾驭 List View 受控组件体系【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappeframework/ui本仓库ui/目录是 Frappe 生态中一套精简的、面向真实业务场景的 Low-Code Web 框架组件库。ui/PHILOSOPHY.md是这套库的“设计规则书”rulebook它定义了三类生成式generative原则在任何未被显式覆盖的场景中应用原则本身就能推导出正确答案当两条原则互相拉扯时原则文本通常会指明裁决依据。本文以该文档为核心骨架结合仓库中的CONTEXT.md词汇表、docs/adr/架构决策记录与src/下的真实源码系统讲解 FP1–FP3 三条核心原则、它们如何塑造 List View 受控组件体系以及作为贡献者/Agent 应如何引用与演进这套规则。读完后你将掌握framework/ui组件 API 的底层设计逻辑并能在实际开发中熟练引用 FP 编号进行代码评审与架构决策。文档定位谁在读、怎么用、与其它文档的关系PHILOSOPHY.md 开篇即明确其受众与使用方式这是理解整份规则的前提受众贡献者、执行 PR 的 AI Agent、代码评审者而非最终用户。用法在 PR 和 Issue 中按编号引用原则例如“这违反了 FP1”“此处适用 FP2 host-owns-persistence”起草新组件或重构旧组件时自上而下通读整份文档当某条原则不再具有生成性——即在真实场景中强制出明显错误的答案——应当提出修改建议而不是悄悄开一个例外。与其它文档的分工PHILOSOPHY.md 明确划定了三份文档的边界这是仓库中一套互相咬合的设计文献体系文档角色内容性质ui/PHILOSOPHY.md设计规则FP1–FP3 生成式原则ui/CONTEXT.md词汇表Sort、Filter、Column、Controlled component、View Snapshot 等术语的精确定义ui/docs/adr/决策记录原则对具体问题的特定应用如 ADR-0007ADR 引用原则原则不引用 ADR特别值得注意的是framework/ui完整继承了frappe-ui的 PHILOSOPHYP1–P14。由于本库的每个组件都在组合 frappe-ui 原子组件frappe-ui 关于命名P1、v-modelP2、primitive propsP3、颜色轴P4、labelingP5、slot 词汇P6/P7、拆分P8、data-* 样式P10、图标P11、可访问性P12和弃用策略P13的规则全部自动适用。本库刻意不重复这些规则只新增framework/ui特有的FP*原则——因为维护两份 P1–P14 的副本恰恰是这些规则想要防止的漂移。这也是为什么本文聚焦 FP1–FP3它们是framework/ui区别于上游的增量部分。FP1组合 frappe-ui 原子组件绝不重建规则在手工实现任何 UI 元素之前先使用frappe-ui的等价组件Dialog、Button、Select、TextInput、Switch、Tabs、TabButtons、ErrorMessage等。framework/ui是一套精简的更高级、与 Frappe 深度集成的组件库——它的职责是把原子组件组合成 doctype-aware 控件而不是重新实现原子组件。当不存在 frappe-ui 等价物而必须构建自定义元素时留下注释说明这个缺口让自定义代码读起来像刻意的回退而不是遗漏的复用。Why原子组件已经内置了 ARIA、键盘导航、焦点管理以及主题化基线对应 frappe-ui 的 P12。重建它们等于重复这套界面、与之发生漂移并丢失上游修复的问题。参见 ui/CLAUDE.md 中对 frappe-ui 与framework/ui的操作性区分。源码印证FileUpload 中的 Tabs 复用ui/CLAUDE.md 给出了一个非常直观的实例FileUpload/FileUploadDialog.vue的来源切换器复用了 frappe-ui 的Tabs组件而不是手写一个 tablist——Tabs提供了 ARIA 与键盘导航且 reka-ui 的unmountOnHide让非激活面板如 CameraSource保持惰性挂载。从仓库结构看这一原则也体现在目录组织上ui/src/components/下的每个组件SortBy/、Filter/、ColumnSettings/、QuickFilter/、FileUpload/、FormLayout/等都遵循“index.ts导出 .vue视图 纯.ts逻辑 stories/演示 tests/单测”的同构布局见 ADR-0002其中的纯逻辑.ts刻意保持 frappe-ui-free从而可以被单元测试直接覆盖。FP2列表视图控件是受控组件——宿主拥有数据获取与持久化规则一个控件SortBy、Filter、Column Settings、Quick Filter 等通过v-model加上一个doctype恰好拥有视图状态的一个切片发出变更事件但绝不触碰任何数据获取资源或持久化层。宿主负责接线数据获取、跨控件同步以及何时/何处保存。库的边界止步于一个可序列化的View Snapshot绝不拥有任何已保存的 View 实体。Why让控件保持 IO 无状态可以使它们在数据与持久化模型各异的应用之间保持可复用同时避免把 CRM 的 “Views” 概念带进共享库。参见 ui/CONTEXT.md 中的Controlled component与View Snapshot词条以及 ADR-0007。受控组件的三层含义结合 ui/CONTEXT.md 的词汇定义FP2 实际上拆解为三层约束控件只拥有自己的状态切片SortBy 的v-model是一组Sort[]{ fieldname, direction: asc | desc }Filter 与 QuickFilter 的v-model是同一个FilterCondition[]数组ColumnSettings 的v-model是Column[]。控件拿到状态、回吐编辑后的状态不关心数据从哪来、存到哪去。共享状态、零事件管线需要互相一致的控件绑定同一个 ref。Filter 与 QuickFilter 同时v-model同一个FilterCondition[]因此一个快速输入与它对应的进阶条件天然同步无需任何事件接线详见 ADR-0005。库止步于 View SnapshotListViewSnapshot是整张列表视图状态的可序列化捕获Filters、Sorts、Columns、surface 的 Quick Filter 字段它是递给宿主持久化层的唯一单元不携带任何身份、所有权或命名 View 概念。一个 View 是 standard / public / pinned / named以及谁拥有它完全属于消费应用。源码印证useListView 的状态所有权边界ui/src/components/ListView/useListView.ts 是 FP2 在代码层面的精确落地。useListView(doctype)自身不持有任何状态——每个切片都住在各自的 co-located composableuseFilters、useSort、useQuickFilter、useColumns里它只是把它们组合成一个带命名空间的面让宿主一眼看清哪个成员驱动哪个控件const view useListView(doctype); view.filters; // { conditions: RefFilterCondition[], wire } view.sort; // { by: RefSort[], orderBy } view.quickFilter; // { fields, customizing, canCustomize } view.columns; // { shown, isCustomized, reset, wire, setWidth, resetWidth } view.snapshot; // ComputedRefListViewSnapshot —— 整张视图作为一个 JSON 对象 view.restore(snapshot); // 从可能不完整的snapshot 播种其中.wire/.orderBy成员是取数投影Frappe 过滤器列表与order_by字符串控件绑定的是状态成员conditions、by、shown、fields宿主用wire成员去查询。snapshot是计算属性仅在控件状态真正变化时才产生新对象因此watch(view.snapshot, …)无需deep: true也不会产生虚假保存。而 ui/src/composables/useDoctypeMeta.ts 则是控件唯一的“外部输入”来源它通过frappe.desk.form.load.getdoctypewith_parent: 1按 doctype 拉取 Meta并用memoizedState按 doctype 字符串做会话级缓存所有调用者共享同一份元数据。FP3从 doctype Meta 派生选项而非应用专属端点规则控件提供的字段选项可排序字段、可筛选字段、列候选应在客户端从 doctype Meta 派生通过共享的useDoctypeMeta而不是从消费应用的专属端点sort_options、filterable_fields等获取。WhyMeta 是每个 Frappe 应用都已有的唯一事实来源从它派生选项能让控件保持应用无关避免把共享库耦合到任何一个应用的 API 表面。参见 ui/CONTEXT.md 中的Field Options与Meta词条。源码印证纯函数式派生FP3 在实现上体现为一系列 frappe-ui-free 的纯函数均可独立单元测试SortBy/getSortOptions.ts→ 从 Meta 字段派生可排序字段的SortOption[]Filter/getFilterableFields.ts→ 派生FilterField[]ColumnSettings/getDefaultColumns.ts→ 依据 Meta 与标题字段生成默认列QuickFilter/getQuickFilterFields.ts→ 依据in_standard_filter标记派生默认快速筛选字段。排序的序列化/反序列化同样是纯函数见 ui/src/components/SortBy/orderBy.tsparseOrderBy(modified desc, name asc)把 Frappe 的逗号连接字符串解析为Sort[]serializeOrderBy是它的逆操作空列表序列化为。三条原则如何协同以 QuickFilter 的“投影”设计为例FP1–FP3 不是孤立的教条它们共同决定了一个复杂控件——QuickFilter——的最终形态。仓库中的 ADR-0005 完整记录了这次决策是理解“原则如何生成答案”的最佳案例FP2 的直接推论QuickFilter 是一个第二个受控组件其v-model就是 Filter 控件绑定的同一个Filter[]数组。useListView(doctype)拥有这个数组连同sorts与 surface 的快速筛选字段并把同一个 ref 交给两个控件。因此 Filter ↔ QuickFilter 的同步自动成立——两者都变更同一个数组不存在两个控件之间的事件管线。这正是 ADR-0001 刻意推迟的共享 composable 的登场时刻。投影是真正的难点在framework/ui的列表模型中一个字段可以携带多个条件amount 100 AND amount 500所以快速筛选不能像 CRM 那样简单拥有filters[fieldname]。一个快速筛选只拥有其字段上、操作符落在该快速筛选的 canonical operator 集合内的条件Check / Select / Date / Datetime 以及Link/ Dynamic Linkequals精确记录选择无切换文本类型Data / Text / Small Text / Long Text与name字段[like, equals]二元集合默认like子串搜索是日常场景但精确匹配必须可达。文本输入的≈/切换按钮以前缀形式渲染在输入框内部单击切换、无菜单name是唯一跨 fieldtype 的特殊情况——它在 Meta 中是 self-Link 却被当作文本筛选因此其切换还会交换值控件like用文本框、equals用 Link 选择器。不破坏精确条件在 Filter 弹层中以集合外操作符构建的条件如Status in [Open, Won]会被快速筛选原样保留——快速输入对它显示为空设置快速筛选时追加一条并存的Status equals …条件而非覆盖。这条“append 而非 replace”的规则直接源于 ADR-0001 的受控纯度快速筛选绝不能破坏它不拥有的精确条件。纯函数化的同步契约quickValue(filters, field)、quickOperator(filters, field)、applyQuick(filters, field, value, operator?)都是纯函数且 frappe-ui-free因此同步契约无需挂载组件即可单元测试。ADR-0005 还记录了若干被否决的选项这些“反面教材”同样由原则裁决给 QuickFilter 自己的v-model让宿主去 reconcile被否决——把投影逻辑推给每个宿主、用source: quick标记条件被否决——把 UI 来源泄漏进 wire-facing 的 Filter 类型、复用弹层的getDefaultOperator被否决——它让 Date 默认between破坏一键 UX等。宿主集成范式View Snapshot 的保存与恢复FP2 的落点就是 ui/src/components/ListView/USAGE.md 中呈现的集成模式。它的核心心智模型正好是三条原则的浓缩受控每个控件只通过v-modeldoctype拥有自己的状态切片从不取数、从不持久化Meta 驱动每个控件在客户端从 doctype 的 Meta 派生 Field Options共享状态、零事件管线需要一致的控件绑定同一个 ref。而宿主你的应用拥有控件刻意不碰的三件事取数、持久化、跨控件接线。useListView替你完成接线useListData按需替你完成取数。单一 watcher 保存一切由于四个控件全部写入同一个view.snapshot对象任何变更都会替换出新值因此只需一个 watcher 就能持久化全部状态watch(view.snapshot, useDebounceFn(saveView, 500)); // (a) 防抖自动保存 // 或 (b) 仅显式保存按钮 function onSaveClick() { saveView(view.snapshot.value); }用户编辑 Filter、调整 SortBy、增删列、拖拽调列宽、编辑 Quick Filter 值、自定义展示哪些快速筛选字段——全部触发这一个 watcher没有其它需要接线的路径。列宽拖拽与增删不是特例它们都编辑view.columns.shown而它就在 snapshot 中。保存到数据库序列化后再调用view.snapshot是富形态条件携带字段 MetaDocType 需要紧凑的 wire 形态——用库的serialize*助手转换后调用你自己的白名单方法import { call } from frappe-ui; import { serializeFilters } from framework/ui/Filter; import { serializeOrderBy } from framework/ui/SortBy; import { serializeColumns } from framework/ui/ColumnSettings; async function saveView(snap: ListViewSnapshot) { await call(my_app.api.save_list_view, { doctype: props.doctype, filters: serializeFilters(snap.filters), // → [[fieldname, op, value], …] order_by: serializeOrderBy(snap.sort), // → modified desc, name asc columns: serializeColumns(snap.columns, fields), // → [{ key, label, width }, …] }); }恢复restore 是部分应用的restore只应用你传入的键其余保持默认值因此它同时兼作按切片恢复view.restore({ columns: parseColumns(saved.columns) }); // 只恢复列filters/sort 不动 view.restore({ filters: parseFilters(fields, saved.filters) }); // 只恢复筛选因为每个控件都是受控的、各自 v-model 自己的切片你甚至可以直接赋值 ref——view.columns.shown.value parseColumns(saved.columns)——完全绕过restore。切片filters / sort / columns / quickFilterFields在两侧都是原子snapshot保存全部restore加载任意子集。若只想存localStorage甚至可以跳过全部助手——snapshot 就是纯 JSONlocalStorage.setItem(key, JSON.stringify(view.snapshot.value))。如何演进这套规则贡献者与 Agent 指南PHILOSOPHY.md 的“使用方式”一节给出了规则自身的演进机制这正是它与普通规范文档的本质区别在 PR / Issue 中按编号引用this violates FP1、FP2 host-owns-persistence applies是评审对话的通行语起草新组件时自上而下通读在开始写代码前把 FP1–FP3 全部走一遍原则失效时提出修订而不是静默例外当一条原则在真实场景中强制出明显错误的答案说明原则本身需要更新——生成式原则的生命力恰恰在于它会自己暴露失效点。实操上仓库还提供了两条补充纪律ui/CLAUDE.md要求*.vue/*.js/*.css等文件遵循.editorconfig的tabsindent_size 4、max_line_length 99并在提交前运行npx prettier --write $(git diff --name-only)story 的演示外壳也应使用 frappe-ui 组件Select而非select、Button而非button既保证视觉一致也是对库本身的 dogfooding。小结ui/PHILOSOPHY.md用三条 FP 原则组合而非重建、受控组件、Meta 派生为framework/ui的整个组件体系设定了可推导的约束边界FP1 守住与上游 frappe-ui 的复用边界FP2 划清库与宿主在取数/持久化上的责任FP3 把选项来源统一到 doctype Meta。三者通过 ui/CONTEXT.md 的词汇表、ui/docs/adr/ 的决策记录与 ui/src/ 的源码实现互相印证构成了 Frappe 前端生态中一份少见的、可直接按编号引用的组件设计契约。对于任何在 Frappe 之上构建列表视图类功能的开发者这套“规则 → 词汇 → 决策 → 代码”的四层文献体系本身就是一份值得复用的设计方法论。【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考