@cgarciagarcia/反应-query-建设者
TypeScript 反应钩钩,用于构建兼容的查询字符串 温泉/泻水-清水-建材。 。 。 .
目录
安装
# npm
npm install @cgarciagarcia/react-query-builder
# yarn
yarn add @cgarciagarcia/react-query-builder
# pnpm
pnpm add @cgarciagarcia/react-query-builder
同伴依赖性 : 反应17,18,或19.
快速启动
import { useQueryBuilder } from "@cgarciagarcia/react-query-builder";
const builder = useQueryBuilder()
builder
.fields('user.name', 'user.last_name')
.filter('age', 18)
.filter('salary', '>', 1000)
.sort('created_at')
.sort('age', 'desc')
.include('posts', 'comments')
.setParam('external_param', 123)
.page(1)
.limit(10)
// Use in fetch
fetch("https://myapi.com/api/users" + builder.build())
// builder.build() returns:
// ?fields[user]=name,last_name&filter[age]=18&filter[salary][gt]=1000&sort=created_at,-age&includes=posts,comments&external_param=123&page=1&limit=10
配置
传递一个可选配置对象到 useQueryBuilder 设置初始状态并自定义行为.
const builder = useQueryBuilder({
// Map frontend field names to backend names
aliases: {
"frontend_name": "backend_name",
},
// Pre-set initial state
filters: [],
includes: [],
sorts: [],
fields: [],
params: {},
// Define mutually exclusive filters (see Advanced section)
pruneConflictingFilters: {},
// Custom delimiters (default: ',')
delimiters: {
global: ',', // applies to all unless overridden
fields: null,
filters: null,
sorts: null,
includes: null,
params: null,
},
// Prepend '?' to the output of build()
useQuestionMark: false,
// Initial pagination state
pagination: {
page: 1,
limit: 10,
},
})
API 参考
所有方法都返回构建者实例,所以它们都是 可连锁。 。 。 .
过滤器
// Add a filter (appends values by default)
builder.filter('status', 'active')
// Add a filter with an operator
builder.filter('salary', '>', 1000)
builder.filter('age', '>=', 18)
// Override existing filter values instead of appending
builder.filter('status', 'inactive', true)
// Remove specific filters
builder.removeFilter('status', 'age')
// Remove all filters
builder.clearFilters()
// Check if filters exist
builder.hasFilter('status') // → boolean
builder.hasFilter('status', 'age') // → true only if ALL exist
可用的运算符 : =, (中文(简体) ), <, (中文(简体) ), >, (中文(简体) ), <=, (中文(简体) ), >=, (中文(简体) ), <>
您也可以导入 FilterOperator 用于类型安全操作器:
import { FilterOperator } from "@cgarciagarcia/react-query-builder"
builder.filter('salary', FilterOperator.GreaterThan, 1000)
字段
builder.fields('name', 'email', 'user.avatar')
builder.removeField('email')
builder.clearFields()
builder.hasField('name') // → boolean
排序
builder.sort('created_at') // default: asc
builder.sort('age', 'desc')
builder.removeSort('created_at', 'age')
builder.clearSorts()
builder.hasSort('created_at') // → boolean
包括
builder.include('posts', 'comments')
builder.removeInclude('posts')
builder.clearIncludes()
builder.hasInclude('posts') // → boolean
参数
builder.setParam('custom_key', 'value')
builder.setParam('ids', [1, 2, 3])
builder.removeParam('custom_key')
builder.clearParams()
builder.hasParam('custom_key') // → boolean
倾斜
const builder = useQueryBuilder({
pagination: { page: 1, limit: 10 }
})
builder.page(3) // go to page 3
builder.nextPage() // page + 1
builder.previousPage() // page - 1 (stops at 1)
builder.limit(25) // change page size
builder.getCurrentPage() // → number | undefined
builder.getLimit() // → number | undefined
说明: 更改过滤器,去掉过滤器,或将限制自动重排到第1页.
公用事业
build()
返回最后的查询字符串.
builder.build() // → "?filter[age]=18&sort=created_at"
toArray()
返回作为平面字符串数组的查询状态。 作为反应查询有用 queryKey。 。 。 .
import { useQuery } from "@tanstack/react-query"
const builder = useQueryBuilder()
const { data } = useQuery({
queryFn: () => getUsers(builder.build()),
queryKey: ['users', ...builder.toArray()],
})
tap(callback)
检查内部状态而不中断链.
builder
.filter('age', 18)
.tap((state) => console.log(state))
.sort('name')
when(condition, callback)
有条件执行基于布尔的回调 。 建造者无论怎样都会返回.
builder.when(isAdmin, (state) => {
console.log('Admin state:', state)
})
高级: 冲突过滤器
一些过滤器在后端是相互排斥的(例如. date 对 between_dates) (中文(简体) ). 使用 pruneConflictingFilters 让库自动处理.
const builder = useQueryBuilder({
pruneConflictingFilters: {
date: ['between_dates'],
// 'between_dates': ['date'] is added automatically (bidirectional)
},
})
builder.filter('date', '2024-08-13')
// → ?filter[date]=2024-08-13
builder.filter('between_dates', ['2024-08-06', '2024-08-13'])
// → ?filter[between_dates]=2024-08-06,2024-08-13
// (date filter was automatically removed)
冲突是 默认双向 - 你只需要声明一次 如果您愿意,也可以明确声明双向.
从 URL 水分
说你有一个列表视图 有过滤器,种类和相片。 用户选择"status = active",按日期排序,打开第3页——并与队友共享链接. 您希望链接能用已经应用的完全相同的过滤器打开 .
这就是一个 适配器 是否:它将建造者连接到一个外部来源(URL, localStorage随便你 内置 createSearchParamsAdapter 处理 URL 大小写.
30秒的例子
import {
useQueryBuilder,
createSearchParamsAdapter,
} from "@cgarciagarcia/react-query-builder";
const builder = useQueryBuilder({
adapter: createSearchParamsAdapter({ sync: true }),
});
builder.filter("status", "active").sort("created_at", "desc");
// URL bar is now: /?filter[status]=active&sort=-created_at
刷新页面,共享链接——过滤器返回. 就是这样.
刚才发生的事:
- 在挂载时,适配器 读取 当前 URL 和种子构建者.
sync: true接下来 挂载后写一次 以最终状态实现 URL 正常化( 所以)BaseConfig默认,urlOmit,或者一个僵化的链接,用户在即时得到调节——不需要突变),以及随后的每一个变化.- URL 输出映射什么
.build()生产,所以你的后端和URL栏都同意.
只读水合( 无 URL 写入)
如果您只想在挂载时水分, 且之后再也不能触碰 URL, 请离开 sync 输出 :
useQueryBuilder({
adapter: createSearchParamsAdapter(),
});
现在 read() 当钩子挂起时运行一次——与 useState(() => …) - 事情就这样结束了 挂载后的 URL 变化不会重新水合 .
自定义写入器
sync 接受三种表格,取决于您希望URL更新有多积极:
// 1) Default behavior: replaceState (no extra browser history entries)
createSearchParamsAdapter({ sync: true }); // alias for "replace"
// 2) pushState (every mutation is a back-button step)
createSearchParamsAdapter({ sync: "push" });
// 3) Bring your own — useful for Next.js / React Router / debouncing
createSearchParamsAdapter({
sync: (search) => router.replace({ search }),
});
挂载时常态化 运行在全部三种模式中——在构建者构建之后,刻出一次,所以URL栏立即反映最终状态. 对于 sync: "push" 第一次通话仍然使用 replaceState (所以上山起火不会增加"幽灵回扣"条目;您的自定义回扣也会在上山上起火一次,如果这让你的路由器感到惊讶,则用关闭旗来守卫.
不要担心其他应用程序的查询参数——编剧 保存着它不承认的任何东西。 。 ?utm_source=newsletter 或者说 ?theme=dark 当过滤器被添加、更新或清除时,保持不动.
与外部变化相对应(builder.rehydrate()(中文(简体) )
read() 运行 一次 在山上。 后出为建州主. 如果有别的东西改变源头——用户点击回/Forward,另一个标签会变异 localStorage,一个websocket推出一个新的服务器侧过滤器——建设者自己不会注意到.
builder.rehydrate() 重新运行适配器 read() ,并将数据层(filters / skinds / 包括 / 字段 /params / pagination)替换为来源现在所说的任何内容。 配置(别名、划界人、, pruneConflictingFilters, (中文(简体) ), useQuestionMark)被保留.
const builder = useQueryBuilder({
adapter: createSearchParamsAdapter({ sync: true }),
});
// Re-pull state from the URL after the browser navigates.
useEffect(() => {
const onPop = () => builder.rehydrate();
window.addEventListener("popstate", onPop);
return () => window.removeEventListener("popstate", onPop);
}, [builder]);
为什么手动而不是自动? 不同来源需要不同的听众(popstate 为历史, storage 用于跨tab localStorage,用于服务器状态的 Websocket订阅,...). 连接右听器属于你的代码,其中的选择是显而易见的. 图书馆别挡道.
异名: 保留您的前端名称, 船后端名称
你可能给UI起个名字userName, (中文(简体) ), createdAt)和 API 中的另一个name, (中文(简体) ), created_at) (中文(简体) ). 过 aliases 到构建器,适配器处理翻译 两个方向 自动 :
useQueryBuilder({
aliases: { userName: "name", createdAt: "created_at" },
adapter: createSearchParamsAdapter({ sync: true }),
});
// Your code keeps using frontend names:
builder.filter("userName", "Jane").sort("createdAt", "desc");
// The URL bar shows backend names (same as .build() would emit):
// /?filter[name]=Jane&sort=-created_at
// On refresh, ?filter[name]=Jane hydrates back as { userName: "Jane" }.
适配器会自动从构建器配置读取别名,所以你只声明一次.
URL 和后端的不同名称
URL和后端不必共享相同的命名. 也许你希望URL能说用户的语言(?filter[documento]=…)而您的 API 保留技术名称( )?filter[code]=…) (中文(简体) ). 或者你的代码 rol 西班牙文,但URL应改为: type 用于共享链接.
过 urlAliases 切换到适配器 aliases数字 :
useQueryBuilder({
aliases: { dni: "code", rol: "type" }, // state → backend (drives .build())
adapter: createSearchParamsAdapter({
sync: true,
urlAliases: { dni: "documento", rol: "type" }, // state → URL bar
}),
});
builder.filter("dni", "12345").filter("rol", "admin");
// URL bar: ?filter[documento]=12345&filter[type]=admin ← user sees this
// .build(): ?filter[code]=12345&filter[type]=admin ← API receives this
相同逻辑属性的三个独立命名空间:
| 层 | 名称 | 由 |
|---|---|---|
| 状态( 您的代码) | dni, (中文(简体) ), rol | 你怎么称呼 .filter() |
| URL 栏 | documento, (中文(简体) ), type | adapter.urlAliases |
| 后端 | code, (中文(简体) ), type | builder.aliases |
奥米 urlAliases 以让 URL 共享构建者的名称(普通例)。 过 urlAliases: {} 明确选择退出任何 URL 翻译 —— 然后 URL 逐字映射您的状态空间名称, 即使当 builder.aliases 已经设定.
安全说明:使用 URL QQ后端,知道后端名的攻击者可以进行手动操作 ?filter[code]=… 直接说 用适配器的许可列表将其锁定 :
adapter: createSearchParamsAdapter({
sync: true,
urlAliases: { dni: "documento", rol: "type" },
allowed: { filters: ["documento", "type"] }, // backend-name attempts → dropped
}),
从 URL 中隐藏噪音状态 : urlOmit
有时建设者会携带您的条目 后端需要, 但用户不应该在 URL 栏中看到。典型案例:默认包含/字段 API 总是需要 (include=organization,permissions那只是繁杂的共享链接.
urlOmit 每个桶只写一个拒绝列表。 所列名字仍留在州内(因此 .build() 他们只是没有到达 URL:
useQueryBuilder({
includes: ["organization", "permissions"], // always sent to API
adapter: createSearchParamsAdapter({
sync: true,
urlOmit: { includes: ["organization", "permissions"] },
}),
});
builder.filter("status", "active");
// .build() → ?filter[status]=active&include=organization,permissions ← backend
// URL bar → ?filter[status]=active ← user
通关卡 "*" 将桶中的每个条目都放下——用于 fields,这是用户在URL中很少需要看到的内部API优化:
adapter: createSearchParamsAdapter({
sync: true,
urlOmit: {
fields: ["*"], // hide ALL fields from URL
includes: ["organization", "permissions"], // hide only these
},
}),
IDE 自动完成建议 "*" 感谢一个字形的 工会打字技巧, 所以它是可以发现的 同时仍然接受任何 简单的属性名称.
异形觉悟于 filters 财务报告和审定财务报表 sorts ——在词汇表(状态或URL)中列出一个名称,两个表单都跳过.
说明数字 : urlOmit 只影响作家。 如果一个精心设计的URL包含其中一个名称,读者仍然会处理. 将相同名称添加到 excludeKeys 如果你也想在水合时拒绝的话.
重新命名 URL 密钥
有时默认的 URL 是动词 :
?filter[status]=active&filter[role]=admin&sort=-created_at&include=author,tags&fields[user]=id,name
这是一个口活——足够长的时间在聊天预览中溢出,丑陋的分享,更难扫描. 重新绘制密钥以缩短 :
createSearchParamsAdapter({
keys: { filter: "filt", sort: "srt", include: "inc", fields: "fld" },
sync: true,
});
现在,同样的状态产生:
?filt[status]=active&filt[role]=admin&srt=-created_at&inc=author,tags&fld[user]=id,name
为什么你可能想要这个:
- 更短,更可分享的链接 ——每个参数可保存字节,当您拥有许多过滤器时,差异很大.
- 清理 URL 栏 ——方便用户和截取支持票的截图.
- 品牌或域语言 - 你的应用程序说"克瑞",不是"过滤器"? 匹配用户已经知道的词汇表 .
- 避免相撞 ——如果周边应用已经使用
?filter=...或者说?sort=...重命名来共存.
读者和作家都使用新密钥,所以来回转接仍然有效. 这个 仅限 影响 URL 栏.. .build() (你传递给 fetch继续用克南克语 filter编号 :sort/... 命名您的后端预期 .
锁定来自 URL 的密钥
URL是用户输入. 没有限制,一个精心设计的链接像 ?filter[is_admin]=true 会直接流入你的 .build() 呼叫和出到你的后端。 你有两个原始人, 每个桶你选一个符合你情况的人
allowed ——严格许可列表("只有这些")
使用它,当你有一个简短的,明确的清单 合法的东西:
createSearchParamsAdapter({
allowed: {
filters: ["status", "role", "created_at"],
sorts: ["created_at", "name"],
params: ["locale", "tenant"], // anything not here is dropped
},
});
省略桶时的默认值 :
filters编号 :sorts编号 :includes编号 :fields~ 允许一切( 这样 URL 可以自由驱动过滤)params- 不允许任何东西(包罗万象是逐一否认的——参数是任意的,所以你必须选择加入,列出它们)
excludeKeys ——定点拒绝名单("除这些之外的一切")
使用它时,你一般相信桶 但想屏蔽一些危险的名字—— 不列举一切合法:
createSearchParamsAdapter({
// No allowed.filters → I accept any filter from the URL.
// I have 47 legitimate filter attributes and I add more every month;
// maintaining a full whitelist is brittle. But is_admin and password
// must NEVER come through the URL.
excludeKeys: {
filters: ["is_admin", "password"],
},
});
我该用哪一个?
| 情况 | 使用 |
|---|---|
| "Short,明确列出什么是允许的" | allowed |
| "接受一切,除了这些" | excludeKeys |
| "白名单加上顶上移动的拒绝名单" | 两者(防御深度) |
如果你准备好了 allowed.filters: ["status", "role"],列表 is_admin 内 excludeKeys.filters 实值 冗余 ——已经因为不允许而放弃了. 它不会打破任何东西(并且读作意向文件),但不会在运行时拉起重量. 当您保持一个稳定的允许列表并想要一个单独的,快速变化的拒绝列表时,该组合是有用的.
适用于两者的细节
-
两者均适用于 读者和作家 对称性地——URL栏总是在您的政策之内.
-
excludeKeys总是胜出allowed(低拍允许). -
异形觉悟于
filters财务报告和审定财务报表sorts。 。 。 . 当你准备好aliases,如果 无论是 uRL 边名(后端) 或 状态边名(前端) 在列表中 。 以您所想的词汇书写您的规则 :aliases: { userName: "name" }, allowed: { filters: ["userName"] } // ✓ allows ?filter[name]=... // — or — allowed: { filters: ["name"] } // ✓ also allows it否认的一方
excludeKeys: { filters: ["is_admin"] }两个块?filter[is_admin]=...和?filter[adminFlag]=...(别名键),因此攻击者不能通过切换词汇来绕过拒绝列表. -
对于
fields,可以通过短道具匹配(password)或通过entity.prop(单位:千美元)user.password) (中文(简体) ). 选择你需要的精确度. -
page财务报告和审定财务报表limit为: 自动水合 当在 URL 中出现,并在 设置中由 作者 释放state.pagination. 圆通出箱-无额外配置. -
为什么每桶?
password是个危险的过滤器 但很好fields选择( 它只是选择 API 返回的哪一列) 。 一个单子会迫使你进入全局或一无所有.
想要另一个来源吗? 写自己的适配器
QueryBuilderAdapter 刚刚 { read, write? }。包罗一切:
// Persist to localStorage instead of the URL
const localStorageAdapter: QueryBuilderAdapter = {
read: () => JSON.parse(localStorage.getItem("filters") ?? "{}"),
write: (state) => localStorage.setItem("filters", JSON.stringify(state)),
};
useQueryBuilder({ adapter: localStorageAdapter });
相同的图案为 react-router 搜索参数,散列路由器,内存商店,索引DB – 您可以从中读取并写入任何内容 .
下层
如果您只想要没有钩子集成的 URL 解析器, 则两块都会导出为纯函数 :
import {
parseSearchParams,
serializeSearchParams,
} from "@cgarciagarcia/react-query-builder";
// URL → state
parseSearchParams("?filter[status]=active&sort=-name");
// → { filters: [...], sorts: [{ attribute: "name", direction: "desc" }] }
// state → URL
serializeSearchParams({
filters: [{ attribute: "status", value: ["active"] }],
});
// → "filter[status]=active"
都接受同样的 keys 编号 : urlAliases 编号 : allowed 编号 : excludeKeys 选项作为适配器.
已知的限制
URL协议使用几个字符作为控制符号. 包含它们的过滤值 沉默地堕落 在来回旅行中——写作者释放出这个值,但读者却将它分成不同的. 在我们解除这个限制之前 把这些当作过滤器内限值:
| 字符 | 为什么它坏了 |
|---|---|
, | 多值分隔器 - filter[tag]=a,b 变成 ["a", "b"] 解析 |
<, (中文(简体) ), >, (中文(简体) ), <=, (中文(简体) ), >=, (中文(简体) ), <> (作为 领导 前缀) | 运算符语法 - filter[age]=>=18 成为运算符 >= + 数值 ["18"] |
其他"特长"人物如: %, (中文(简体) ), &, (中文(简体) ), + 和空间是好的——它们游走 URL-逃逸 和在环行无恙.
如果您需要这些字符中的任何一个( 如带有逗号的标记), 请在 URL 适配器外保留该部分状态, 或者在传递到您侧面之前避开它 .filter()。 。 。 .
支助
有问题或需要帮助吗? 打开讨论 (原始内容存档于2018-09-29). on GitHub.
考虑支助
如果此软件包帮助您, 请考虑支持其创建者 :
付费页 : @carlosgarciadev : 翻译:
许可证
麻省理工学院许可证(MIT). 见 自由主义 更多信息.