PostgreSQL18的pgadmin中有一个ERDTool.jsx有1132行,这个体量理论上说非常庞大,但做过现实工程的都知道,其实只能算重组件中的mini尺寸了。pgadmin功能并不算有多丰富,怎么还是做成这样呢,当然不是维护团队不会拆分,毕竟还是做了199个jsx的。
首先映入眼帘的是registerEvents对19个EventBus的监听,这东西让人倒吸一口凉气,其中最醒目的莫过于this.eventBus.registerListener(ERD_EVENTS.DOWNLOAD_IMAGE, this.onImageClick),名字就不对劲,实现能如何呢?那么看一下这个onImageClick():
onImageClick() {
this.setLoading(gettext('Preparing the image...'));
/* Move the diagram temporarily to align it to top-left of the canvas so that when
* taking the snapshot all the nodes are covered. Once the image is taken, repaint
* the canvas back to original state.
* Code referred from - zoomToFitNodes function.
*/
this.diagramContainerRef.current?.classList.add('ERDTool-html2canvasReset');
const margin = 10;
let nodesRect = this.diagram.getEngine().getBoundingNodesRect(this.diagram.getModel().getNodes());
let linksRect = this.diagram.getBoundingLinksRect();
// Check what is to the most top left - links or nodes?
let topLeftXY = {
x: nodesRect.getTopLeft().x,
y: nodesRect.getTopLeft().y
};
if(topLeftXY.x > linksRect.TL.x) {
topLeftXY.x = linksRect.TL.x;
}
if(topLeftXY.y > linksRect.TL.y) {
topLeftXY.y = linksRect.TL.y;
}
topLeftXY.x -= margin;
topLeftXY.y -= margin;
let canvasRect = this.canvasEle.getBoundingClientRect();
let canvasTopLeftOnScreen = {
x: canvasRect.left,
y: canvasRect.top
};
let nodeLayerTopLeftPoint = {
x: canvasTopLeftOnScreen.x + this.diagram.getModel().getOffsetX(),
y: canvasTopLeftOnScreen.y + this.diagram.getModel().getOffsetY()
};
let nodesRectTopLeftPoint = {
x: nodeLayerTopLeftPoint.x + topLeftXY.x,
y: nodeLayerTopLeftPoint.y + topLeftXY.y
};
let prevTransform = this.canvasEle.querySelector('div').style.transform;
this.canvasEle.childNodes.forEach((ele)=>{
ele.style.transform = `translate(${nodeLayerTopLeftPoint.x - nodesRectTopLeftPoint.x}px, ${nodeLayerTopLeftPoint.y - nodesRectTopLeftPoint.y}px) scale(1.0)`;
});
// Capture the links beyond the nodes as well.
const linkOutsideWidth = linksRect.BR.x - nodesRect.getBottomRight().x;
const linkOutsideHeight = linksRect.BR.y - nodesRect.getBottomRight().y;
this.canvasEle.style.width = this.canvasEle.scrollWidth + (linkOutsideWidth > 0 ? linkOutsideWidth : 0) + margin + 'px';
this.canvasEle.style.height = this.canvasEle.scrollHeight + (linkOutsideHeight > 0 ? linkOutsideHeight : 0) + margin + 'px';
setTimeout(()=>{
let width = this.canvasEle.scrollWidth + 10;
let height = this.canvasEle.scrollHeight + 10;
let isCut = false;
/* Canvas limitation - https://html2canvas.hertzen.com/faq */
if(width >= 32767){
width = 32766;
isCut = true;
}
if(height >= 32767){
height = 32766;
isCut = true;
}
toPng(this.canvasEle, {width, height, pixelRatio: this.state.preferences.image_pixel_ratio || 1})
.then((dataUrl)=>{
DownloadUtils.downloadBase64UrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
}).catch((err)=>{
console.error(err);
let msg = gettext('Unknown error. Check console logs');
if(err.name) {
msg = `${err.name}: ${err.message}`;
}
pgAdmin.Browser.notifier.alert(gettext('Error'), msg);
}).then(()=>{
/* Revert back to the original CSS styles */
this.diagramContainerRef.current.classList.remove('ERDTool-html2canvasReset');
this.canvasEle.style.width = '';
this.canvasEle.style.height = '';
this.canvasEle.childNodes.forEach((ele)=>{
ele.style.transform = prevTransform;
});
this.setLoading(null);
if(isCut) {
pgAdmin.Browser.notifier.alert(gettext('Maximum image size limit'),
gettext('The downloaded image has exceeded the maximum size of 32767 x 32767 pixels, and has been cropped to that size.'));
}
});
}, 1000);
}
这种百行级函数,存在合理性且不论,无论如何它都不应该叫xxClick了,毕竟谁敢相信它所有的代码都是为了完成一个导出png功能?
当然只笼统的说它完成了「一个功能」,那也是委屈它了,这函数实质上究竟做了什么?
- 状态管理: setLoading。
- DOM 劫持: 直接操作样式和类名。
- 复杂的几何计算: 处理包围盒(Bounding Box)。
- IO 操作: 生成图片并触发下载。
- 异常处理: 恢复状态弹出 notifier 警告。
每一项都是焦点,如果换成Java,这段代码还能再套上10个trycatch膨胀到500行,当然如果换成Java必然能规矩许多,不至于如此粗糙。
当然这函数远不止违法单一原则那么简单,几何计算中 margin = 10 和屏幕坐标转换逻辑非常硬核且粗糙,几乎宣判了这块UI已经不可更改了,同时否定了缩放/偏移变化,非常容易出 off-by-one 错误,已经消耗极大了,不想做复杂只想简单实现也可以克隆DOM做一个离屏渲染,还不需要关心什么margin偏移。
toPng还是html-to-image的,这种场景用这个本身就如同儿戏,而且既然都做这么复杂了,哪怕直接再补上一套原生代码,手动绘制,全丢这函数里,不用任何库,这段代码也不会更丑了。
检测到图片到了浏览器 canvas 限制,就直接剪裁+警告,不做一个执行前popup确认和zoom,可以说有些不可理喻了,现实中这个警告几乎不可能弹出来,因为符合的这个逻辑时,其占用的原始内存将达到惊人的 4GB,做这种巨型 DOM 树时UI会进行密集的像素计算。而且计算是同步的,会直接锁死浏览器主线程!程序早已卡死,一行代码都别想执行了。
还有setTimeout为什么 1000ms?为什么不是 500 或 2000?这是典型的“等它渲染完”的 hack,因为修改 transform / width/height 后,浏览器需要时间重排/重绘,html2canvas才能捕获正确内容。用requestAnimationFrame 循环检查或MutationObserver / ResizeObserver来检测实际变化完成不好么?
最后还是回到名字上,一个函数如果叫“xx点击”,它就没有资格去负责“计算并导出32767像素的位图”。
不过综合来说,这东西整体上也算勉强还行了,毕竟它是一个最终节点组件,而且基本上不太可能被依赖,只是功能性问题,不像它旁边那个1560行有15个useEffect的ResultSet.jsx,那都不能叫组件了,那是试图给React塞一个子系统,等PostgreSQL20发布,估计就没人敢改它了。还有用1300行的FormInput.tsx管理着FormIcon、StyledGrid、FormInput、InputSQL、FormInputSQL等子组件的超级组件,这几乎是想做一套扩展UI库。