Skip to content

087. 模块注册思路与渲染钩子 ​

学习目标 ​

这一节把列扩展、表格模块扩展和渲染钩子的关系梳理清楚。

学完后,你应该能理解:

  • 不考虑 TypeScript 时,列扩展的本质是什么;
  • TypeScript 在扩展设计里解决什么问题;
  • 表格模块为什么要变成数组注册;
  • 模块的 seq 和 key 分别有什么用;
  • 为什么有些模块要能删除;
  • 渲染钩子解决什么问题;
  • 查询表单和搜索栏如何通过渲染钩子组合。

不考虑 TypeScript 的列扩展 ​

先把 TypeScript 拿掉,只看运行逻辑。

列扩展本质上很简单。

通用表格要做两件事:

  1. 根据列的 type 找到默认值填充函数;
  2. 用默认值填充后的列覆盖 render 行为。

例如:

ts
const defaultColumnMap = {
  input: fillInputDefaults,
  select: fillSelectDefaults,
  toggle: fillToggleDefaults,
}

处理列时,根据 column.type 找到对应函数。

新增列时,只要往这个映射里新增一项。

这就是列扩展最核心的运行模型。

TypeScript 解决的是安全问题 ​

不用 TypeScript 也能写出扩展逻辑。

但会很危险。

例如:

  • column.type 写错不会提前提示;
  • 下拉列忘记 options 不会提前提示;
  • 图片列忘记传高度不一定能提前发现;
  • 默认值函数参数写错也不容易发现。

TypeScript 的作用是给这些扩展关系加约束。

它让“新增一种列”不仅能运行,还能在使用时有类型提示和错误提醒。

所以这里不是为了炫技写类型。

而是扩展系统越复杂,类型越能保护长期维护。

表格模块扩展的运行模型 ​

表格模块扩展和列扩展类似。

只是列扩展处理的是列配置。

表格模块处理的是整个表格对象。

运行模型可以理解成:

ts
const autoTable = {}

installConfigModule(autoTable)
installStateModule(autoTable)
installHandlerModule(autoTable)
installRenderModule(autoTable)

每个模块执行后,都会往 autoTable 对象上添加自己的能力。

例如配置模块添加配置能力,状态模块添加状态能力,渲染模块添加渲染能力。

从写死安装到数组注册 ​

后面模块会从写死调用变成数组注册。

结构类似:

ts
const modules = [
  { seq: 1, key: "config", use: useConfigModule },
  { seq: 2, key: "state", use: useStateModule },
  { seq: 3, key: "handler", use: useHandlerModule },
]

主体函数会先排序,再依次执行。

ts
modules
  .sort(bySeq)
  .forEach(item => item.use(autoTable))

这样外部项目就可以继续往数组里加模块。

seq 的作用 ​

seq 表示模块顺序。

模块之间有依赖关系。

例如状态模块需要读取运行配置。

所以配置模块必须先安装。

如果顺序错了,后面的模块会读不到前面模块提供的能力。

这和后端服务注册、插件系统、构建流程很像。

先有基础配置,再有依赖配置的功能模块。

key 的作用 ​

key 是模块标识。

它后面主要用于覆盖和删除。

例如已有一个模块叫 aiFill。

某个项目不希望启用 AI 填写功能,就可以根据 key 找到它并移除。

某个项目想替换默认查询表单,也可以根据 key 找到原模块,再覆盖成自己的模块。

所以 seq 管顺序。

key 管身份。

为什么模块要能删除 ​

不是所有项目都能使用所有能力。

例如 AI 填写功能可能会把表格数据发给大模型。

这对某些安全要求高的项目不合适。

如果 AI 能力是一个独立模块,项目就可以选择删除它。

删除后:

  • 按钮不会出现;
  • 事件不会注册;
  • 请求不会发生;
  • 数据不会被发送出去。

这就是模块化带来的可控性。

渲染钩子 ​

表格除了逻辑模块,还需要渲染扩展。

渲染钩子可以理解成一个可排序的内容插槽。

它允许不同模块往同一区域添加内容。

例如表格主体可以分成几块:

  • 查询表单区域;
  • 搜索栏区域;
  • 筛选和排序条件展示区域;
  • 表格主体区域。

每一块都可以用渲染钩子组织内容。

渲染钩子的元素结构 ​

渲染钩子里的每一项可以包含:

  • seq:顺序;
  • key:标识;
  • content:渲染内容。

content 可以是 React 节点,也可以是返回 React 节点的函数。

类似:

ts
interface RenderMatter {
  seq: number
  key: string
  content: ReactNode | (() => ReactNode)
}

后续渲染时,先根据 seq 排序,再把内容渲染出来。

为什么允许 null ​

有些模块在某些状态下不渲染内容。

但它仍然需要占据一次注册位置。

所以渲染钩子允许传入 null。

这样在依赖数组或渲染顺序里,注册数量能保持稳定。

真正输出时,再过滤掉空内容。

这能减少因为数组长度变化带来的不稳定。

状态写法和 useRef ​

渲染钩子内部需要一个可变数组。

它的对象引用要保持稳定。

课程里用了一种类似 useRef 的写法。

区别是避免每次都写 .current。

可以把它理解成:

ts
const renderMatters = []

每次渲染前清空数组。

各个模块重新往里面注册内容。

最后统一排序并渲染。

核心目标是:同一轮渲染里收集所有模块贡献的内容。

稳定排序 ​

渲染钩子要按 seq 排序。

如果两个内容的 seq 一样,最好保持它们原来的注册顺序。

所以课程里使用插入排序。

插入排序在这里的价值不是性能。

而是稳定。

同样顺序值的内容,不会被打乱原始先后关系。

Body Render ​

bodyRender 是整个表格主体区域的渲染钩子。

它可以收集多块内容。

例如:

  • 查询表单;
  • 搜索栏;
  • 表格;
  • 其他模块插入的内容。

这些内容不是写死在一个 JSX 结构里。

而是由不同模块分别注册到 bodyRender。

最后统一渲染。

搜索栏渲染钩子 ​

搜索栏内部也可以继续拆成渲染钩子。

例如搜索栏里可以包含:

  • 搜索框;
  • 查询表单开关按钮;
  • 新建按钮;
  • 更多按钮;
  • 筛选条件展示。

搜索栏模块可以先注册搜索框和基础按钮。

查询表单模块也可以往搜索栏里注册一个按钮,用来控制查询表单显示或隐藏。

这就是嵌套渲染钩子的效果。

查询表单模块 ​

查询表单模块做两件事。

第一,往表格顶部注册查询表单内容。

第二,往搜索栏注册一个按钮。

按钮点击后,控制查询表单是否显示。

所以一个模块不一定只往一个地方加内容。

它可以同时影响多个渲染钩子。

这也是模块扩展比普通组件插槽更强的地方。

调整显示顺序 ​

因为每个渲染内容都有 seq。

所以可以很方便地调整顺序。

例如把查询表单放到最上面:

ts
seq: -1

把它放到最后:

ts
seq: 999

调用方可以按自己的项目习惯调整区域顺序。

不需要改通用表格内部代码。

覆盖与删除 ​

这一节主要实现添加和排序。

覆盖与删除还没有完全整理完。

但思路已经清楚。

通过 key 找到某个渲染项或某个模块。

然后可以:

  • 替换它;
  • 删除它;
  • 调整它的顺序;
  • 插入新的内容。

后面模块组织器会继续完善这个能力。

和 Slot 的区别 ​

渲染钩子有点像插槽。

但它不是一个固定位置只能放一块内容。

它更像一个可排序、可追加、可覆盖的内容队列。

多个模块可以往同一个区域加内容。

最终由渲染钩子统一排序输出。

这比普通 children 或单个 slot 更适合复杂表格组件。

这一节的核心 ​

通用表格的扩展体系分成两层。

第一层是列扩展。

它通过 type 和默认值函数扩展字段显示与编辑能力。

第二层是模块扩展。

它通过模块数组和渲染钩子扩展表格整体功能。

表格越复杂,越不能只靠写死 JSX 和 props。

需要把“谁提供能力”“能力放在哪里”“顺序如何控制”“能否删除覆盖”都设计出来。

AI Agent 课程学习文档。