用 NestJS + Refine 构建快速且高度可定制的 Admin 管理面板
本文基于 Refine 仓库中的博客教程 2021-10-4-admin-panel-with-nestjs.md 展开,完整讲解如何搭建一个 job-posting(职位发布)应用:后端用 NestJS + TypeORM + nestjsx/crud 快速生成 CRUD REST 接口,前端用 Refine 框架构建配套的管理面板。读完后你将掌握 NestJS 的迁移脚本配置、CRUD 自动端点机制,以及 Refine 的 Data Provider、Resources、表格页与路由组织的完整实战方案,并能对照仓库中最新的 examples/blog-job-posting 示例理解版本演进后的写法。
项目结构:api 与 admin 两部分
整个项目拆分为两个独立部分:
- api:NestJS 编写的 REST 后端,使用 TypeORM 连接 MySQL,借助 nestjsx/crud 自动生成
/companies、/jobs等 CRUD 端点; - admin:Refine 构建的管理面板,通过
@refinedev/nestjsx-crud数据 Provider 调用上述 API,渲染公司(companies)与职位(jobs)两个资源的管理页面。
仓库中保留的管理端示例位于 examples/blog-job-posting,包含 src/pages/companies、src/pages/jobs 两个资源目录与入口文件 src/App.tsx,其接口模型定义在 src/interfaces/index.d.ts:ICompany 包含 id、name、location、isActive 及若干可选社媒链接字段,IJob 包含 id、title、location、content、isActive 以及关联的 company 对象——这正是文章示例页面的数据契约。
需要说明:原文档基于 Refine 3.x 编写,仓库中的示例已升级到新版本 API(组件从 @refinedev/antd 导入、资源使用路由路径而非组件)。本文以原文档为骨架,并用当前仓库源码补充 Provider 的底层实现。
构建 NestJS REST API
初始化项目
前置要求:Node(>= 10.13.0,v13 除外)与 npm。通过 Nest CLI 创建项目:
mkdir job-posting-app
cd job-posting-app
npm i -g @nestjs/cli
nest new api
选择 TypeORM 作为 ORM(TypeScript 编写,与 Nest 配合成熟,支持 MySQL、MariaDB、Postgres 等多种数据库),本文选用 MySQL:
npm install --save @nestjs/typeorm @nestjs/config typeorm mysql2
数据库配置与迁移脚本
教程要求完成以下配置(这些文件位于教程配套仓库的 api/ 目录,此处说明其职责与内容):
.env.example:保存数据库连接信息(主机、端口、用户名、密码、库名),实际.env从该模板复制而来;docker-compose.yml:为 MySQL 提供容器化运行环境,保证本地一键启动数据库;ormconfig.ts:TypeORM CLI 的配置文件,供迁移(migration)命令读取;package.json中的迁移脚本:
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"db:migration:generate": "npm run typeorm -- migration:generate",
"db:migration:run": "npm run typeorm -- migration:run",
"db:migration:revert": "npm run typeorm -- migration:revert",
"db:refresh": "npm run typeorm schema:drop && npm run db:migration:run"
}
db:migration:generate 根据实体定义生成增量迁移文件,db:refresh 则直接重建整个 schema,适合开发期快速迭代。
- 导入 TypeOrmModule:在
app.module.ts中通过TypeOrmModule.forRoot(配置项指向ormconfig.ts/ 环境变量)注册 TypeORM。
安装 nestjsx/crud 生成 CRUD 端点
npm i @nestjsx/crud @nestjsx/crud-typeorm class-transformer class-validator
nestjsx/crud 会在每个实体的 Controller 上自动挂载标准 REST 端点(GET /resource 列表、GET /resource/:id 详情、POST /resource 创建、PATCH /resource/:id 更新、DELETE /resource/:id 删除、POST /resource/bulk 批量创建等),并支持通过查询参数实现过滤、排序、分页与联表。Entity、Controller、Service 的具体创建过程在教程中略过,可参考配套示例仓库。
构建 Refine Admin Panel
用脚手架生成项目
npm create refine-app@latest admin -- -b v3
交互选项按如下回答(对应原文档的问卷输出):
Select your project type › refine-react
What will be the name of your app · admin
Package manager: · Npm
Do you want to use a UI Framework? · Ant Design
Do you want a customized theme?: · Yes (Custom Variables)
Router Provider: · React Router v6
Data Provider: · nestjsx-crud
Auth Provider: · None
Do you want to add example pages? · Yes (Recommended)
Do you want a customized layout? · Yes
i18n - Internationalization: · No
随后 cd admin && npm run dev 即可看到 Refine 示例应用。在 App.tsx 中把后端地址指向前面搭建的 API:
const API_URL = "http://localhost:3000";
编写 companies 列表页
为 companies 端点添加列表页 admin/src/pages/companies/list.tsx(继承原文档完整代码,并补充关键注释):
import {
List,
Table,
TextField,
useTable,
IResourceComponentsProps,
getDefaultSortOrder,
Space,
EditButton,
DeleteButton,
TagField,
ShowButton,
} from "@pankod/refine";
import { ICompany } from "interfaces";
export const CompanyList: React.FC<IResourceComponentsProps> = () => {
// useTable 内部调用 dataProvider.getList,
// 将 pagination/sorters/filters 序列化为 URL 查询参数
const { tableProps, sorter } = useTable<ICompany>({
sorters: {
initial: [{ field: "id", order: "desc" }], // 默认按 id 倒序
},
});
return (
<List>
{/* tableProps 展开出 dataSource、pagination、onChange 等 */}
<Table {...tableProps} rowKey="id">
<Table.Column
dataIndex="id"
key="id"
title="ID"
render={(value) => <TextField value={value} />}
defaultSortOrder={getDefaultSortOrder("id", sorter)}
sorter
/>
<Table.Column
dataIndex="name"
key="name"
title="Name"
render={(value) => <TextField value={value} />}
defaultSortOrder={getDefaultSortOrder("name", sorter)}
sorter
/>
<Table.Column
dataIndex="location"
key="location"
title="Location"
render={(value) => <TextField value={value} />}
defaultSortOrder={getDefaultSortOrder("location", sorter)}
sorter
/>
<Table.Column
dataIndex="isActive"
key="isActive"
title="Is Active"
render={(value) => <TagField value={value} />}
defaultSortOrder={getDefaultSortOrder("status", sorter)}
sorter
/>
<Table.Column<ICompany>
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
{/* recordItemId 指定操作对象,按钮跳转/请求对应记录 */}
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
<DeleteButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
核心机制:useTable 钩子返回 tableProps(含 dataSource、分页状态与 onChange 处理),把 Ant Design Table 的交互状态与 Refine 的数据请求打通;getDefaultSortOrder 将 Refine 侧的排序状态同步回列头,实现受控排序。创建、编辑页(create.tsx、edit.tsx)同理使用 useForm/useUpdate 等钩子实现,jobs 资源照此办理。
在 App.tsx 中定义资源
import { Refine } from "@pankod/refine";
import routerProvider from "@pankod/refine-react-router";
import nestjsxCrudDataProvider from "@refinedev/nestjsx-crud";
import "styles/antd.less";
import {
CompanyList,
CompanyShow,
CompanyCreate,
CompanyEdit,
} from "./pages/companies";
import {
Title,
Header,
Sider,
Footer,
Layout,
OffLayoutArea,
} from "components";
import { JobList, JobCreate, JobEdit } from "pages/jobs";
function App() {
const API_URL = "http://localhost:3000";
const dataProvider = nestjsxCrudDataProvider(API_URL);
return (
<Refine
dataProvider={dataProvider}
Title={Title}
Header={Header}
Sider={Sider}
Footer={Footer}
Layout={Layout}
OffLayoutArea={OffLayoutArea}
routerProvider={routerProvider}
resources={[
{
name: "companies",
list: CompanyList,
create: CompanyCreate,
edit: CompanyEdit,
show: CompanyShow,
},
{
name: "jobs",
list: JobList,
create: JobCreate,
edit: JobEdit,
show: CompanyShow,
},
]}
/>
);
}
name 字段必须与后端端点路径一致(/companies、/jobs),因为 Data Provider 正是以 resources/name 拼接请求 URL;Title、Header、Sider、Footer、Layout、OffLayoutArea 六个布局组件构成可定制化的面板骨架。
深入 @refinedev/nestjsx-crud:Provider 如何对话 nestjsx/crud
上文示例中 nestjsxCrudDataProvider(API_URL) 的实际实现位于 packages/nestjsx-crud/src/provider.ts。它接收 apiUrl 与一个可选的 AxiosInstance(默认使用库内置实例),返回一个实现 Required<DataProvider> 全部方法的对象,核心设计是:用 nestjsx 官方的 RequestQueryBuilder(来自 @nestjsx/crud-request)把 Refine 的查询语义翻译成服务端能识别的查询参数。以 getList 为例:
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const url = `${apiUrl}/${resource}`;
let query = RequestQueryBuilder.create();
query = handleFilter(query, filters); // 过滤条件
query = handleJoin(query, meta?.join); // 联表
query = handlePagination(query, pagination); // 分页
query = handleSort(query, sorters); // 排序
const { data } = await httpClient.get(`${url}?${query.query()}`);
// 兼容返回裸数组(无分页)与 { data, total }(有分页)两种响应
if (Array.isArray(data)) {
return { data, total: data.length };
}
return { data: data.data, total: data.total };
},
这套转换链路正是 useTable 中 sorters、分页与过滤"即插即用"的底层原因,各环节实现位于 packages/nestjsx-crud/src/utils:
- 过滤:handleFilter.ts 将 Refine 的
CrudFilters递归转换为 nestjsx 的SCondition,其中叶子条件形如{ field: { [operator]: value } },and/or组合条件则递归映射为$and/$or;算子转换由mapOperator负责(例如$gt与 nestjsx 的gt对齐)。test/utils/handleFilter.spec.ts 等测试用例覆盖了这一映射; - 其余方法:
getMany用id IN [...]条件批量查询;update发送PATCH /resource/:id;updateMany与deleteMany以Promise.all并发循环逐条请求;createMany走 nestjsx 的POST /resource/bulk批量端点;deleteOne为DELETE /resource/:id;错误统一经transformHttpError转成 Refine 的HttpError结构(HTTP 状态码 + 消息),供notificationProvider展示; custom方法:额外支持useCustom场景,可传自定义url、method(put/post/patch/delete/get),get 请求会同样应用 filter/join/sorter 参数。
也就是说,nestjsx/crud 在服务端自动生成端点,@refinedev/nestjsx-crud 在前端把 Refine 的抽象查询翻译成与之匹配的参数约定,两端由 @nestjsx/crud-request 的查询语言精确对接,这是本方案"前后端零胶水代码"的关键。
当前仓库示例的版本演进
对照 examples/blog-job-posting/src/App.tsx 可以看到教程代码在仓库中的最新形态:
- 组件改从
@refinedev/antd导入(List、TextField、TagField等),核心能力来自@refinedev/core; resources声明由"资源名 → 组件"改为"资源名 → 路由路径"(如list: "/companies"、edit: "/companies/edit/:id"),页面通过<Routes>/<Route>显式挂载;- 布局改用
<ThemedLayout>包裹<Outlet>的方式,配合RefineThemes.Blue主题、useNotificationProvider通知与UnsavedChangesNotifier、DocumentTitleHandler等辅助组件; - 数据 Provider 的用法保持不变,仍是一行
nestjsxCrudDataProvider(API_URL)。
新版列表页 examples/blog-job-posting/src/pages/companies/list.tsx 与原文档的 v3 版本在逻辑上几乎一一对应:同样的 useTable 初始排序、同样的五列布局与操作按钮,只是导入来源更新。本地快速体验可用:
npm create refine-app@latest -- --example blog-job-posting
小结
这篇教程展示了一条高效的管理面板开发路线:NestJS + TypeORM 迁移脚本 + nestjsx/crud 自动端点负责后端,Refine + nestjsx-crud Data Provider 负责前端,两者通过 nestjsx 的请求查询语言无缝衔接。仓库中的 packages/nestjsx-crud 源码(provider 实现与单元测试)和 examples/blog-job-posting 示例是理解该方案的最佳一手资料;需要注意原文档基于 Refine v3 编写,实际落地时应以仓库内最新示例的 API 形态为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00