Skip to content

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

学习目标

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

学完后,你应该能理解:

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

不考虑 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 课程学习文档。