npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nvoo/schema

v1.0.0

Published

Nvoo — Schema-driven UI engine. One declaration drives table, search form, mobile card, and beyond.

Readme

@nvoo/schema

Schema-driven UI 契约层 + 列表查询状态机。

一份 schema 声明,多端独立渲染器消费(PC 表格 / H5 卡片 / 查询表单 / 工具栏 / 行内操作)。渲染器实现在 @nvoo/ui-layoutpc / h5 子入口。


设计哲学

  • 协议与渲染解耦:本包只定义"数据形状"和"状态机",不引入任何 UI 组件。
  • 跨端复用同一份 schemacontrol 是控件语义名(如 "select"),由各端 Renderer 自行映射到具体组件(PC → a-select、H5 → van-picker)。
  • 声明优先,逃生口完备:99% 场景靠声明式配置完成;剩下 1% 边缘场景提供 render / transform / normalize 等函数逃生口。

目录


安装

pnpm add @nvoo/schema
# peerDep
pnpm add vue@^3.5.0

package.jsonexportsdevelopment 条件,Vite dev 模式下直接走源码,无需预构建。


快速上手

import { useListQuery, createListAdapter } from "@nvoo/schema";

// 1. 用 adapter 把后端真实接口适配成统一协议
type User = { id: number; name: string; status: number };

const fetchUsers = createListAdapter<
  { content: User[]; count: number }, // 后端原始响应
  { pageNum: number; pageSize: number; username?: string }, // 后端原始入参
  User
>({
  request: params => api.get("/users", { params }),
  keys: {
    currentKey: "pageNum",
    pageSizeKey: "pageSize",
    dataKey: "content",
    totalKey: "count"
  }
});

// 2. 用 useListQuery 持有列表状态
const {
  data,
  total,
  current,
  pageSize,
  loading,
  hasMore,
  query,
  search,
  changePage,
  loadMore,
  refresh,
  reset
} = useListQuery<User>({
  request: fetchUsers,
  defaultPageSize: 20,
  initialQuery: { status: 1 }
});

useListQuery 不关心 UI 渲染;PC Renderer 与 H5 Renderer 各自消费它,业务侧也可独立使用(脱离 schema 体系)。


核心能力

| 能力 | 说明 | | -------------------- | ------------------------------------------------------------------------------- | | 双数据策略 | overwrite(PC 表格,翻页覆盖)/ append(H5 触底加载,翻页追加) | | 后端协议适配 | 字段名变换 / dot 嵌套路径 / query 嵌入指定 key / 完全自控 transform & normalize | | 跨端 schema 契约 | 一份声明同时驱动表格列、查询表单、H5 卡片、行内操作、工具栏按钮 | | 字典一体化 | 列声明 dict: "user_status" 自动派生到表格单元格 / 表单控件 / H5 标签 | | 控件语义化 | control: "select" 而非 component: ASelect,保证跨端可复用 | | 函数式逃生口 | render / transform / normalize / confirm 函数式签名覆盖边缘场景 |


Composables API

useListQuery

列表查询核心状态机,与 UI 完全解耦。

function useListQuery<T = Record<string, unknown>>(
  options: UseListQueryOptions<T>
): UseListQueryReturn<T>;

UseListQueryOptions<T>

| 字段 | 类型 | 默认 | 说明 | | ----------------- | ------------------------- | ------------- | ----------------------------- | | request | ListRequestFn<T> | — | 必填,统一签名的请求函数 | | defaultPageSize | number | 10 | 初始每页条数 | | initialQuery | Record<string, unknown> | {} | 初始查询条件(如默认近 7 天) | | strategy | "overwrite" \| "append" | "overwrite" | 数据策略:PC 表格 / H5 触底 |

UseListQueryReturn<T>

| 字段 | 类型 | 说明 | | ------------ | -------------------------------------- | ---------------------------------------------- | | data | Ref<T[]> | 当前页数据(append 下为累计) | | total | Ref<number> | 总条数 | | current | Ref<number> | 当前页码(1-based) | | pageSize | Ref<number> | 每页条数 | | loading | Ref<boolean> | 加载状态 | | hasMore | ComputedRef<boolean> | data.length < total,触底判断用 | | query | Record<string, unknown> | 当前查询条件,reactive 可双向绑定 | | search | () => Promise<void> | 重置到第 1 页并查询 | | changePage | (current, pageSize) => Promise<void> | 翻页(overwrite 专用) | | loadMore | () => Promise<void> | 触底加载下一页并追加(仅 append) | | refresh | () => Promise<void> | overwrite 重取当前页;append 重置到第 1 页 | | reset | () => Promise<void> | 清空 query 回到 initialQuery 并查询 |

注意search / reset 总是覆盖数据;loadMore 在非 append 模式下是 no-op。


createListAdapter

后端协议适配器工厂。把后端原始签名(Spring Pagecontent/countpageNum/pageSize 等)转换为本框架统一的 ListRequestFn<T> 协议。

function createListAdapter<RawResp, RawReq extends Record<string, unknown>, T>(
  options: ListAdapterOptions<RawResp, RawReq, T>
): ListRequestFn<T>;

ListAdapterKeys

字段映射配置,所有字段都可选:

| 字段 | 默认 | 说明 | | ------------- | ------------ | -------------------------------------------------- | | currentKey | "current" | 入参当前页字段名,如 "pageNum" / "page" | | pageSizeKey | "pageSize" | 入参每页条数字段名,如 "size" / "perPage" | | queryKey | undefined | query 是否嵌入到指定 key(不设默认 spread 到顶层) | | dataKey | "data" | 出参数据数组路径,支持 dot:"result.list" | | totalKey | "total" | 出参总条数路径,支持 dot:"result.total" |

ListAdapterOptions<RawResp, RawReq, T>

| 字段 | 类型 | 优先级 | | ------------------ | --------------------------------------- | ---------------------- | | request | (params: RawReq) => Promise<RawResp> | — | | keys | ListAdapterKeys | 基础映射 | | transformRequest | (params: ListRequestParams) => RawReq | 高于 keys | | normalize | (raw: RawResp) => ListResponse<T> | 最高,完全自控出参 |

适配示例

// 场景 1:扁平字段名变换
createListAdapter({
  request: api.list,
  keys: { currentKey: "pageNum", dataKey: "content", totalKey: "count" }
});

// 场景 2:嵌套路径
createListAdapter({
  request: api.list,
  keys: { dataKey: "result.records", totalKey: "result.total" }
});

// 场景 3:query 嵌入指定 key(后端 DTO 形如 { pageNum, pageSize, query: {...} })
createListAdapter({
  request: api.list,
  keys: { queryKey: "query" }
});

// 场景 4:完全自控(逃生口)
createListAdapter({
  request: api.list,
  transformRequest: p => ({
    page: p.current,
    size: p.pageSize,
    ...p.query
  }),
  normalize: raw => ({
    data: raw.data.items,
    total: raw.data.totalCount
  })
});

Schema 类型契约

ListPageSchema

一份声明驱动 PC 表格 / H5 卡片 / 查询表单 / 工具栏。

interface ListPageSchema<T> {
  columns: ColumnConfig<T>[]; // 列声明
  request: ListRequestFn<T>; // 数据请求函数
  rowKey?: keyof T & string; // 行 key(默认 "id")
  actions?: ActionConfig<T>[]; // 工具栏按钮(新增/导出/批量删除)
  defaultPageSize?: number; // 默认 10
  mobileMode?: "card" | "list"; // 移动端展示,默认 "card"
  rowSelection?: RowSelectionConfig<T>; // 行选择配置
}

RowSelectionConfig

| 字段 | 默认 | 说明 | | ------------------ | ------- | --------------------------------------- | | type | "M" | M 多选 / S 单选 / N 关闭 | | keepSelected | false | 翻页是否保留已选 | | getCheckboxProps | — | 行级禁用判断 | | onChange | — | 选择变化通知(Renderer 自管 keys/rows) |


ColumnConfig

一份列声明派生四处视图:PC 表格列、PC 查询表单(从 search 派生)、H5 卡片字段、H5 状态标签。

| 字段 | 类型 | 说明 | | --------------- | ----------------------------------- | ---------------------------------------------------------------------- | | title | string | 列标题 | | dataIndex | string \| [string, string] | 单字段或范围合并列(渲染成 "start ~ end") | | valueType | ColumnValueType | text/number/date/dateTime/select/tags/switch | | dict | string | 字典 key,运行时通过 useDictionary 拉取。一处声明 → 三处自动派生 | | dictStatusMap | Record<string, NvStatus> | 字典项着色补充(字典未返回 status 时) | | valueEnum | ValueEnum | 静态枚举(逃生舱,无字典场景) | | hideInTable | boolean | PC 表格隐藏 | | hideInMobile | boolean | 移动端卡片隐藏 | | mobileTitle | boolean | 移动端卡片标题字段(建议仅 1 个) | | width | number \| string | 列宽(数字→px) | | align | "left" \| "center" \| "right" | 默认 left | | ellipsis | boolean | 内容超出省略 | | fixed | "left" \| "right" | 固定列 | | search | false \| FormFieldConfig | 派生查询字段,false/不配不参与查询 | | render | (value, row, index) => VNodeChild | 自定义单元格渲染(逃生口) | | actions | ActionConfig<T>[] | 列级操作按钮组(声明后该列变成 ActionCell) |

优先级:dict > valueEnumactions 一旦配置,valueType/render/dict/valueEnum 失效。


FormFieldConfig

查询表单 / 编辑表单通用字段声明,跨端复用。

| 字段 | 类型 | 说明 | | -------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | control | string | 控件语义名:内置 input/textarea/number/select/switch/date/dateRange/dateTime/dateTimeRange;或业务自定义(通过 registerFormControl 注册) | | field | string \| string[] | 提交参数名;范围查询用 [start, end] | | label | string | 标签文案 | | placeholder | string \| [string, string] | 占位文案;不填自动派生 | | defaultValue | unknown | 默认值;范围类型是 tuple | | dict | string | 字典 key(select 优先) | | options | OptionItem[] | 静态选项(无 dict 时) | | remote | RemoteOptionSource | 远程搜索(优先级低于 dict) | | props | Record<string, unknown> | 控件 props 透传 | | primary | boolean | H5 顶栏快捷搜索(仅第一个 primary 生效) | | hidden | (formValues) => boolean | 条件隐藏 | | transform | (value, formValues) => Record<string, unknown> | 值 → 提交参数的最终变换 |

数据源优先级:dict > options > remote。 解析顺序:local controls prop > global registry > 内置默认。


ActionConfig

工具栏按钮 / 行内操作 / 卡片右下角操作共用一份声明。

| 字段 | 类型 | 说明 | | --------------------- | -------------------------------------------- | --------------------------------------------- | | key | string | 操作 key(埋点 / 测试 selector) | | label | string | 按钮文字 | | icon | string \| VNodeChild \| (() => VNodeChild) | 图标 | | attrs | Record<string, unknown> | antd a-button 属性透传(仅工具栏生效) | | status | (ctx) => boolean | 是否显示(默认恒显示) | | confirm | ConfirmConfig \| ConfirmFn | 确认弹窗:配置对象或函数式逃生口 | | handler | (ctx) => unknown \| Promise<unknown> | 点击回调,返回 Promise 时自动 refresh | | refreshAfter | boolean(默认 true) | handler 完成后是否自动 ctx.refresh() | | clearSelectionAfter | boolean(默认 false) | handler 完成后是否自动 ctx.clearSelection() |

ActionContext

| 字段 | 说明 | | ---------------- | ------------------------------------- | | record | 行内场景:当前行;工具栏:undefined | | selectedRows | 工具栏:已选行;行内:[] | | refresh | 触发列表刷新 | | clearSelection | 清空行选择 |

典型 refreshAfter: false 场景:查看详情、复制文本、打开弹窗。 典型 clearSelectionAfter: true 场景:批量删除 / 批量发布。


通用 API 协议

types/common/api.ts 定义了跨页面复用的请求/响应类型:

interface ListRequestParams {
  current: number; // 1-based
  pageSize: number;
  query: Record<string, unknown>; // search 展开后填充
}

interface ListResponse<T> {
  data: T[];
  total: number;
}

type ListRequestFn<T> = (params: ListRequestParams) => Promise<ListResponse<T>>;

interface ApiResponse<T> {
  // 通用后端响应包装(按需用,非强制)
  code: number;
  data: T;
  message?: string;
}

设计原则与约定

  1. schema 不指定具体组件 —— 用 control 语义名代替 component,否则破坏跨端复用。需要换组件走 Renderer 的 registry。
  2. 优先用 dict,慎用 valueEnum —— 字典是动态的(运行时拉取),枚举是静态的。除非真的没有任何后端支撑,否则一律 dict
  3. 范围查询统一用 tuple —— field: ["startTime", "endTime"],控件返回的 tuple 自动拆开。
  4. 状态机与 UI 解耦 —— useListQuery 不引用任何 Vue 组件;可在测试 / Node 环境 / 非 schema 项目独立使用。
  5. 逃生口要收敛使用 —— render / transform / normalize 是兜底,不是首选。大量使用说明 schema 设计需要补字段。

文件结构

src/
├── index.ts                      # 统一出口
├── composables/
│   ├── useListQuery.ts           # 列表查询状态机
│   └── createListAdapter.ts      # 后端协议适配器工厂
└── types/
    ├── index.ts                  # 类型统一出口
    ├── common/                   # 跨页面原子协议
    │   ├── api.ts                # ListRequestParams / ListResponse / ListRequestFn
    │   ├── form.ts               # FormFieldConfig / FormControlType / OptionItem
    │   └── action.ts             # ActionConfig / ActionContext / ConfirmConfig
    └── list-page/                # 列表查询页领域协议
        ├── column.ts             # ColumnConfig / ValueEnum
        ├── schema.ts             # ListPageSchema / RowSelectionConfig
        └── index.ts

License

MIT