ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Handsontable 18.0 升级到 18.1 迁移指南:许可证密钥、列头排序交互与单遍渲染的 7 项变更

Handsontable 18.0 升级到 18.1 迁移指南:许可证密钥、列头排序交互与单遍渲染的 7 项变更 Handsontable 18.0 升级到 18.1 迁移指南许可证密钥、列头排序交互与单遍渲染的 7 项变更【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址: https://gitcode.com/gh_mirrors/ha/handsontableHandsontable 18.1 是一次 minor release不删除任何公共 API但仍有 7 项变更需要你逐一核对其中 1 项会直接阻塞网格缺失或无效的许可证密钥、3 项改变了既有行为列头点击排序、布局计算方式、loading 插件两个选项的渲染方式、1 项让 18.0 中被静默压制的通知重新出现最后 2 项仅影响设置了 [sanitizer] 选项的用户。读完本文你将能对照谁受影响—如何迁移逐项完成升级并了解这些变更背后的源码实现逻辑。版本概况无 API 删除但 7 项变更需要关注在动手升级前先明确 18.1 的边界它是一个 minor release没有移除任何公共 API因此大部分应用可以直接平滑升级。需要你留意的 7 项变更分布如下变更性质变更内容阻塞性变更缺失或无效的许可证密钥会以不可关闭的模态框阻塞网格行为变更3 项列头点击的排序时机与响应区域网格布局改为单遍渲染并预测滚动条loading 插件的title/description由 HTML 改为纯文本渲染通知恢复过期密钥检测重新生效通知重新出现sanitizer 相关2 项传给 sanitizer 的两个source值发生变化sanitizer 将看到两个此前从未见过的内容来源完整变更清单见 Changelog。下面逐项展开。1. 在每个环境中设置许可证密钥阻塞性变更变更内容在 18.1 中缺失或无效的许可证密钥会阻塞网格Handsontable 用一个无法关闭的模态框盖住网格并在控制台重复输出错误信息。而 18.0 对同样两种状态缺失 / 无效只在网格下方增加一行提示网格保持完全可用。需要澄清的是两个不会触发阻塞的状态过期密钥不阻塞任何功能订阅过期的密钥同样不阻塞超过到期日后控制台会报错但所有功能照常工作付费客户永远不会被锁在门外。如果你已经持有有效密钥无需任何操作18.0 下有效的密钥升级到 18.1 后继续有效。密钥的有效性只取决于密钥自身的过期日期与运行中的 Handsontable 版本无关。谁受影响只要有任何 Handsontable 实例在没有有效密钥的环境下运行你就会受影响。实践中通常是以下几种情况你从未设置过 [licenseKey]例如因为 18.0 允许网格在下方提示下继续工作你在生产环境设置了licenseKey但在本地开发、CI、端到端测试套件或 Storybook / demo 构建中没有设置——这些是最可能在升级后翻车的场景因为阻塞会出现在此前没有人会去看提示的位置你设置了一个 Handsontable 无法读取的密钥例如被截断的密钥、属于其他产品的密钥或空字符串这类占位值。如何迁移为每个环境中的每个实例传入有效密钥。商业使用传入你购买的密钥const hot new Handsontable(container, { licenseKey: your-license-key, });HotTable licenseKeyyour-license-key /hot-table [settings]{ licenseKey: your-license-key }/hot-tablehot-table :settings{ licenseKey: your-license-key }/hot-table非商业或评估使用传入非商业密钥。它是一个合法的密钥因此不会阻塞网格const hot new Handsontable(container, { licenseKey: non-commercial-and-evaluation, });HotTable licenseKeynon-commercial-and-evaluation /hot-table [settings]{ licenseKey: non-commercial-and-evaluation }/hot-tablehot-table :settings{ licenseKey: non-commercial-and-evaluation }/hot-table在源码中可以看到non-commercial-and-evaluation是LicenseKeyFormat联合类型的一个正式成员与其他密钥格式entitlement、legacy、unknown并列见 entitlementLicenseKey/types.ts这从实现层面印证了它是一条合法、可被识别的密钥。提示Handsontable 只在实例初始化时读取一次许可证密钥。通过 [updateSettings()] 传入新的licenseKey不会重新评估许可证。要应用不同的密钥请重新创建实例。2. 更新响应列头点击的代码变更内容点击列头在 18.0 中会在mouse down时立即排序且响应区域是整个表头单元格。18.1 改为在mouse up时排序并且只有表头标签label及其排序指示器响应点击。按下标签以外的表头区域只会选中该列而不会排序——正是这种设计让你可以在同一手势中选中列并拖动它。Handsontable 通过指针是否移动来区分点击与拖拽。同一变更还影响了列移动相关的两个钩子[beforeColumnMove] 和 [afterColumnMove] 现在只在实际拖动列时触发。18.0 中即使没有移动任何列普通的表头点击也会触发这两个钩子。谁受影响符合以下任一情况就会受影响你有自动化测试或脚本通过向列头发送mousedown事件来排序或点击的是表头单元格而不是其标签你用beforeColumnMove/afterColumnMove来检测表头点击而非真正的列移动。如何迁移——模拟表头点击发送完整的按下 释放事件并且瞄准表头标签。只要点击标签会触发排序标签元素就带有sortAction类。迁移前const header hot.rootElement.querySelector(thead th:nth-child(2)); header.dispatchEvent(new MouseEvent(mousedown, { bubbles: true }));迁移后const header hot.rootElement.querySelector(thead th:nth-child(2)); const sortLabel header.querySelector(.colHeader.sortAction); sortLabel.dispatchEvent(new MouseEvent(mousedown, { bubbles: true })); sortLabel.dispatchEvent(new MouseEvent(mouseup, { bubbles: true }));两个事件之间不要移动指针。指针一旦移动手势就会变成列拖拽而不是排序。sortAction这个类名在源码中有明确定义HEADER_ACTION_CLASS sortAction见 columnSorting/domHelpers.ts并且只在对应列可点击排序时附加单元测试 domHelpers.unit.ts 专门验证了这一行为。如果你的测试驱动的是真实浏览器直接点击标签元素本身。点击表头单元格中心不再触发排序因为标签按内容自适应尺寸并不会填满整个单元格。如何迁移——把列移动钩子当点击处理器用把逻辑迁移到仍然会在点击时触发的钩子上用 [afterColumnSort] 响应排序或用 [afterOnCellMouseUp] 响应点击本身。迁移前const hot new Handsontable(container, { afterColumnMove(movedColumns, finalIndex, dropIndex, movePossible, orderChanged) { // 以前在普通表头点击时也会执行 trackHeaderInteraction(); }, });迁移后const hot new Handsontable(container, { afterColumnSort(currentSortConfig, destinationSortConfigs) { trackHeaderInteraction(); }, });让beforeColumnMove和afterColumnMove回归它们现在所描述的场景真正的列移动。3. 预期过期密钥的通知会重新出现变更内容18.1 重新启用了过期许可证密钥检测。在 18.0 中这段检测逻辑静默地从未执行因此过期密钥既不显示提示也不输出控制台消息。升级之后超过日期的密钥会在网格下方显示一条提示并在控制台报错。不阻塞任何功能所有特性照常工作。这不是配置变更引起的——提示出现只是因为检测恢复了。谁受影响如果你用日期已过的密钥运行 18.1就会受影响。对于永久密钥指的是维护日期早于你所运行 Handsontable 版本的构建日期对于订阅密钥指的是到期日已经过去。如何迁移代码上无需任何改动。要移除提示续期并传入新密钥即可。可将迁移工作与第 1 节的每个环境设置有效密钥一并完成。4. 检查自定义布局代码网格出现错误滚动条变更内容18.1 改为单遍渲染基于缓存的列宽与行高预测是否出现滚动条然后只渲染一次。而 18.0 是先渲染网格、测量结果、再根据测量决定布局。只要单元格按 Handsontable 缓存的尺寸渲染预测结果就与 DOM 一致。当两者不一致时网格可能出现不需要的滚动条、缺失需要的滚动条或视口短了一行或一列。[mergeCells] 插件会自动为你关闭这条新路径——合并单元格的高度依赖于正在计算的视口预测无法解析这种依赖因此它选择回到旧的测量路径。从源码可以完整还原这条机制钩子定义modifySinglePassLayout在钩子常量中登记自 18.1.0 起可用文档明确说明单遍渲染根据行/列尺寸和容器盒预测滚动条是否出现然后只渲染一次返回false强制走旧的先测量再渲染路径见 core/hooks/constants.ts默认开启singlePassLayout配置以函数形式求值——() this.hot.runHooks(modifySinglePassLayout, true)每次读取时求值见 tableView.ts插件退出mergeCells在初始化时注册钩子this.addHook(modifySinglePassLayout, this.#onModifySinglePassLayout)mergeCells.ts其处理函数直接返回false#onModifySinglePassLayout () falsemergeCells.ts。谁受影响当单元格的渲染尺寸在渲染前不可知时你就会受影响你编写了插件其内容尺寸依赖于正在计算布局的视口——这正是mergeCells选择退出的情形。没有其他插件会替你退出你使用自定义渲染器或自定义 CSS在 Handsontable 测量之后改变单元格的盒子尺寸例如覆盖行高的样式规则。如果你的网格在 18.1 下与 18.0 渲染结果一致本节不适用于你也无需任何配置变更。如何迁移从 [modifySinglePassLayout] 钩子返回false为该实例恢复 18.0 的布局路径const hot new Handsontable(container, { modifySinglePassLayout() { return false; }, });从插件内部注册钩子方式与mergeCells一致this.addHook(modifySinglePassLayout, () false);由于 Handsontable 在每次布局时都会重新读取这个钩子见上文tableView.ts中每次求值的singlePassLayout通过 [updateSettings()] 启用或禁用插件无需重建网格即可生效。5. loading 插件的title和description以纯文本渲染变更内容[loading] 插件的title和description选项现在按纯文本渲染。18.0 将两者都写成 HTML因此传入的标记会被解释执行现在标记会原样显示。插件的icon选项保持不变——它是唯一接受标记的槽位正因如此你才能替换默认的 SVG 加载动画。导出进度对话框的标题也以同样方式转义。它来自语言字典因此只有包含标记的自定义翻译才会受此影响。单元测试印证了这一行为加载内容测试用textContent断言标题与描述验证诸如Loaded 5 10 rows这类包含尖括号的文本被当作纯文本渲染见 loading/tests/content.unit.js。谁受影响如果你在loading.title或loading.description中传入了标记或在传给插件 [show()] / [update()] 方法的title、description中传入了标记就会受影响。常见的例子是两行之间的br或包裹标题片段的strong。如何迁移去掉标记改用 CSS 控制文本样式。标题渲染进.ht-loading__title描述渲染进.ht-loading__description。迁移前const hot new Handsontable(container, { loading: { title: Loading strongsales data/strong, description: Step 1 of 3brThis can take a minute, }, });迁移后const hot new Handsontable(container, { loading: { title: Loading sales data, description: Step 1 of 3. This can take a minute., }, });如果加载状态确实需要标记请把它放进icon。6. 更新基于source参数分支的 sanitizer本节与下一节都关于 [sanitizer] 选项。如果你没有设置 sanitizer这两节都与你无关。你的 sanitizer 收到的两个source值发生了变化。嵌套表头测量innerHTML变为header用于测量嵌套表头宽度的离屏offscreen渲染此前以innerHTML调用你的 sanitizer而正式渲染的表头以header调用。同一个标签以两个名字到达你的 sanitizer于是上下文感知的 sanitizer 会对它应用两套不同的规则测出的宽度与用户看到的实际内容无法匹配。现在两者统一使用header。innerHTML不再被网格的任何部分传递。对话框内容undefined变为dialog对话框内容此前传给 sanitizer 时根本没有第二个参数因此source是undefined。现在它是dialog。如果你的 sanitizer 用switch或if/else链按source路由对话框内容会从原来处理undefined的分支通常是 default 分支移出进入你可能没有写过的dialog分支。如果对话框内容需要与默认处理不同的规则请补充该分支。谁受影响只有当你的 sanitizer 检查第二个参数时才会受影响。忽略该参数的 sanitizer 无需改动已经把所有未知来源路由到 catch-all 分支的 sanitizer 对对话框也照常工作。如何迁移删除innerHTML分支。你已经写好的header分支现在同时覆盖两条路径。迁移前const hot new Handsontable(container, { sanitizer: (content, source) { if (source header || source innerHTML) { return strict(content); } return loose(content); }, });迁移后const hot new Handsontable(container, { sanitizer: (content, source) { if (source header) { return strict(content); } return loose(content); }, });7. 预期 sanitizer 会看到两个此前从未见过的内容来源有两个写入 HTML 的地方此前不经过 sanitizer。这是缺陷而不是契约变更[sanitizer] 选项的文档契约是覆盖 Handsontable 代你写入的 HTML而配置了 sanitizer 的网格在这些位置本应得到保护却没有。18.1 修复了这两处。这两处修复不需要任何操作来保持正确性你的代码也不会因此停止工作。本节之所以存在是因为一个做得比剥离标记更多的 sanitizer 现在会看到它从未见过的内容其中一种情况有一个值得了解的后果。password单元格来源password由 [password] 单元格类型渲染的单元格此前直接写入 DOM不经过 sanitizer现在会经过它。渲染出的值通常是hashSymbol组成的一串字符本身不含标记因此大多数 sanitizer 会原样返回。只有当你的 sanitizer 会重写纯文本或你通过自定义valueFormatter或包含标记的hashSymbol自行生成显示值时才需要检查。Handsontable 自己的剪贴板负载来源CopyPaste.paste.sourceData当你在两个 Handsontable 实例之间复制时网格会在text/html之外写入第二条剪贴板条目。它携带单元格背后的源数据这正是对象值单元格能作为对象而非其显示文本到达目标端的原因。该条目此前解析时不经过你的 sanitizer。由于任何页面都可以从自己的复制处理器写入同一种剪贴板类型即使网格配置了 sanitizer精心构造的剪贴板也能绕过检查到达解析器。现在它会被消毒——这是本版本的安全修复。如果你设置了sanitizer并且自己设置了 [parsePastedValue]或使用了autocomplete、dropdown、multiSelect列你就会受影响。后三种单元格类型会为你自动开启parsePastedValue所以即使你从未写过这个选项也可能受影响。一个会剥离不安全标记的 sanitizer如 DOMPurify会保留负载中的表格结构一切不变一个改为转义 HTML 的 sanitizer 会把表格变成文本粘贴的单元格收到的将是显示值而非原始对象。如何迁移如果你采用转义而非剥离并且希望对象值粘贴继续工作请放行这一个来源const hot new Handsontable(container, { sanitizer: (content, source) { if (source CopyPaste.paste.sourceData) { return content; } return escapeHtml(content); }, });这是安全的。该负载会被解析进一个惰性文档inert document无法加载资源或执行脚本因此放行它不会让你暴露在恶意剪贴板注入之下。它拥有独立的source值正是为了让你可以做出这个选择而不削弱对真正粘贴 HTML 的处理。如果你的任何列都不解析粘贴值保持其被消毒也完全没有问题——这意味着你的 sanitizer 会看到每一份剪贴板负载这对做得比过滤标记更多的 sanitizer 很重要。变更汇总表变更谁受影响需要的操作缺失或无效的许可证密钥以不可关闭的模态框阻塞网格任何在没有有效密钥环境下运行的实例包括本地、CI、测试和 demo 环境传入有效的 [licenseKey]非商业使用传入non-commercial-and-evaluation列排序在 mouse up 时执行且只有表头标签及其排序指示器响应点击通过向表头单元格派发mousedown来排序的测试或脚本在.colHeader.sortAction标签上派发mousedown和mouseup指针不要移动beforeColumnMove和afterColumnMove不再在普通表头点击时触发用任一钩子检测表头点击的代码改用 [afterColumnSort] 或 [afterOnCellMouseUp]过期许可证密钥检测恢复使用日期已过密钥的实例代码无需改动。续期即可移除提示网格单遍渲染并预测滚动条而非渲染后测量内容尺寸依赖视口的插件以及测量后改变单元格尺寸的自定义渲染器或 CSS若布局表现不同从 [modifySinglePassLayout] 返回false恢复 18.0 路径loading插件的title/description以文本而非 HTML 渲染在任一选项或传给show()/update()的title/description中传入标记的网格去掉标记用 CSS 样式化.ht-loading__title与.ht-loading__description标记放进iconsanitizer 对嵌套表头测量收到header而非innerHTML对对话框内容收到dialog而非undefined依赖第二个参数分支的 sanitizer删除innerHTML分支如对话框内容需要新增dialog分支password单元格与 Handsontable 自己的剪贴板负载现在经过 sanitizer设置了sanitizer的网格剪贴板负载还涉及 [parsePastedValue] 或autocomplete、dropdown、multiSelect列无需操作除非你的 sanitizer 转义 HTML 而非剥离。如是则放行CopyPaste.paste.sourceData结果完成以上核对与调整后你的应用即可运行在 Handsontable 18.1 上每个环境都有有效密钥列头交互与你的测试保持一致自定义布局与 sanitizer 按新的契约工作。相关资源许可证密钥指南行排序指南列移动指南安全指南loading 指南变更日志【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址: https://gitcode.com/gh_mirrors/ha/handsontable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进