一、大屏从九块涨到四十七块以后前年年初我们把镇上的数字乡村大屏交付上线那时候一共九块页面每块页面里挂三到五个可视化组件渲染任务几乎全部交给 ECharts 5 完成。到今年三月页面数量涨到了四十七块组件类型也从单一的图表扩成了地理、三维、媒体、动画、页面容器五个族地理族用 Mars3D 3.6 带一个三维地球三维族用 Three.js 0.159 自己拼场景媒体族管视频和轮播。前端主包体积从一点二兆涨到四点六兆展厅一体机上的首屏白屏时间从一点一秒拖到了三点四秒讲解员每次开场都得先等上三四秒画面才出来。比体积更难受的是组件的接入成本。每加一类组件我们都要回到渲染主循环里改一遍代码在类型判断链上追加一个分支再去处理它的初始化、配置、尺寸变化和卸载。地理组件补进来那次前后动了渲染主循环、配置面板、事件总线和样式表四个文件改完还要把之前二十多块页面逐块回归。那次回归用掉两个下午测试同学在三个页面里发现地图容器盖住了图例而渲染主循环对地图图层自己的层级逻辑一无所知。页面多起来之后出错的形态也变了。组件初始化抛异常时主循环捕获不到整块容器就空在那里控制台里只留一行看不懂的堆栈运维同事在展厅现场没法判断到底是数据接口没回来还是组件根本没加载。我们需要的是一套能明确告诉调用方“这个名字不存在”的机制而不是让它安静地什么都不画让排查从第一分钟就失去方向。二、问题出在渲染层的分支判断上把现象拆开之后根因并不复杂。渲染入口当时用一个长 if-else 链按组件类型字符串决定走哪条初始化路径。这个写法在只有图表一种组件的时候完全够用一旦组件族变多它就把两类不同知识混进了同一个函数一类是“这个组件叫什么、接受什么数据、默认配置是什么”另一类是“这个组件怎么初始化、怎么销毁”。前者是业务配置后者是引擎适配两者的变更节奏差了大概一个数量级。直觉做法是继续加分支或者把公共部分抽成工具函数但这两条路都绕不开同一个事实渲染入口必须认识每一种组件。只要它还认识新增组件族就一定会碰它回归面就一定压不下去。我们真正想要的是让渲染入口只认识一件事一个名字以及这个名字对应的配置和生命周期钩子。至于名字背后是 ECharts 实例还是 WebGL 上下文渲染入口不需要知道也不应该知道。还有一个容易忽略的细节组件的卸载方式天生不一样。ECharts 实例要调 resize 和 disposeThree.js 的 renderer 要显式回收几何体、材质和渲染器Mars3D 的地图对象要移除图层和事件监听纯 DOM 组件可能什么都不用做。这些差异如果散落在主循环的各个分支里任何一次改动都会牵动到别的组件页面上任何一处小问题都会让人怀疑是不是渲染层又被改坏了。三、两条路我们都试过第一条路是把大屏做成 Vue 的动态组件用 component 标签配一份全局对照表谁用谁登记。这条路改造成本小一个星期就能把主循环里的分支拆干净但它有两个我们接受不了的代价。动态组件默认会走打包器的静态分析地理和三维两类重引擎很难真正按需加载主包体积只降了不到两成Vue 的组件生命周期和大屏的渲染生命周期也不是一回事组件被 keep-alive 缓存之后dispose 钩子什么时候触发变得难以预测。第二条路是注册表加工厂。每个组件模块在加载时向全局注册表登记三样东西名字、它能接受的数据字段类型、以及一份默认配置同时提供一个异步加载函数。渲染入口只做一次查表拿到条目之后按统一的钩子名调用 mount、update、resize、dispose。这条路的代价也很明确登记动作必须显式发生任何一个组件模块忘了被引入查表时就会报错类型约束只能靠运行时校验编译期给不了太多保护。还有一条我们只做了原型就放弃的路是把每个组件塞进 iframe 里做沙箱隔离。隔离确实彻底一个三维组件把浏览器搞崩也不会影响同屏的图表但跨文档通信的延迟在展厅滚动播报的场景下肉眼可见而且每个 iframe 都要重新加载一份引擎代码内存占用翻了好几倍。权衡的结果是选第二条路把一个组件族的复杂度锁进它自己的模块让渲染入口退化成一张表。四、注册表长什么样注册表的接口压到了四个方法register 用来登记get 用来查表has 用来判断存在性list 用来给配置面板出可选清单。登记项一共七个字段name、family、acceptFields、defaults、events、loader 和 lifecycle。acceptFields 用一个字符串数组描述这个组件能消费什么形状的数据柱状图登记的是 dimension 和 measure地理组件登记的是 geoJson 和 center三维组件登记的是 modelUrl 和 camera。这套注册表现在装在万村乐数字乡村的大屏引擎里先服务九块村务页面。查表的失败路径是这段代码里改得最认真的一处get 拿不到条目时不再返回空对象而是抛一个带组件名和已登记清单的错误前端捕获之后把容器换成一块带提示的占位卡同时把这行错误上报到前端日志接口。上线之后白屏问题从每周三到五次降到几乎为零剩下的几次都是数据接口超时跟渲染层没关系了。热插拔是后来才验证的。镇上要求在地理大屏上叠一层地块边界我们新写了一个 geo-plot 组件登记进注册表然后在页面配置里把原来图表组件的位置改成 geo-plot整个过程没有打开渲染主循环那个文件。这是注册表在设计阶段真正想换来的东西让新增组件族的代价固定在组件模块内部而不是扩散到框架代码里也不需要每次都靠人工圈定回归范围。五、几个必须写死的约定生命周期钩子的签名统一成 mountcontainerconfigctx、updateconfig、resize和 dispose其中 ctx 里带着当前页面的数据源句柄和事件总线。组件要响应点击和鼠标移入时在登记项的 events 数组里声明事件名注册表在挂载完成后按声明逐个绑定避免组件自己去猜容器上有没有监听。尺寸变化一律走 ResizeObserver并且做了两百毫秒防抖因为展厅一体机在滚动播报时容器宽度每帧都在变。层级用一张常量表管住地图层固定 200图表层 100浮层和弹窗 900三维画布单独一档放在 50。这组数字不是拍脑袋定的是踩过层级争抢之后倒推出来的地图要压住图表弹窗要压住地图而三维画布作为底图放在最下面让其他组件能叠上去做标注。所有组件挂载时必须引用这套常量不允许在任何组件的样式里写死层级否则问题会在某个特定页面里重新长出来。六、踩过的三个坑第一个坑是三维组件的显存泄漏。现象是展厅一体机连续切换大屏半小时之后开始掉帧切到第六十次左右整个页面卡死强制刷新能恢复但过半小时又出现。根因是 Three.js 的 WebGLRenderer 不会在容器被移除时自动回收资源几何体、材质、贴图都还挂在显存里我们只把 canvas 从 DOM 上摘掉了。改法是给三维组件实现 dispose 钩子在里面显式释放 geometry、material、texture 和 renderer注册表在卸载流程里确保这个钩子一定会被调到。改完之后的验收方式是连续切换两百次显存占用稳定在四百兆上下不再上涨。第二个坑是图表在容器尺寸变化后错位。展厅那台大屏有一块侧边栏会在讲解时收起收起动画走完之后ECharts 实例还按初始化时的宽度在画图例跑到容器外横轴标签挤成一团。根因是实例只在 mount 时读了一次 offsetWidth之后再没同步过。改法是注册表统一接管尺寸观测在容器上挂 ResizeObserver回调里防抖两百毫秒后调用组件的 resize 钩子组件内部再调 ECharts 的 resize 方法。顺带把图表初始化时机从挂载后立即执行改到下一帧避免拿到零宽度的容器。第三个坑是地图图层和图表容器的层级争抢。现象是点击地图上的图例弹出详情时弹窗被地图底下那一层盖住鼠标点不到按钮。根因是地图引擎会在容器里自己插一层脱离文档流的定位元素并且带一个不低的层级而我们的弹窗层级是按老页面的写法定在五百。改法是前面提到的那张常量表把地图层、图表层、浮层分开弹窗统一提到九百并且要求弹窗挂到最外层容器而不是组件内部。这类问题在页面少的时候看不出来页面一多同一个错误会在不同页面反复出现。七、这层解决不了什么注册表只能管住组件的加载、配置和销毁它管不了数据。同一份地块边界数据图表组件和地理组件需要的形状完全不一样转换逻辑还是得写在各自的适配层里注册表最多通过 acceptFields 在挂载前做一次形状校验把明显不匹配的调用拦在初始化之前。跨组件的联动状态也不在这层比如点击某个村组之后同屏另外三块图表要跟着筛这件事我们后来交给了页面级的事件总线组件之间的耦合并没有因为注册表而消失只是没有落进框架代码。调试体验是另一个代价。查表机制让调用链多绕了一层一个组件没渲染出来排查顺序变成了先看注册表里有没有这条登记再看 loader 返回的模块对不对最后才看组件内部。我们给开发环境加了一个注册表快照接口能在控制台一次性打出当前所有已登记组件和它们的来源文件这一层排查时间从十几分钟压到了两分钟以内。至于渲染性能查表本身的开销可以忽略四十七块页面里登记的组件一共十九种一次 Map 查找还不到微秒级。八、小结把渲染入口退化成一张表这件事本身不复杂难的是愿意承认框架代码不应该认识每一种组件。我们最后留下的约定只有三条新组件必须显式登记登记项必须写清接受的字段和默认配置生命周期钩子必须实现完整包括 dispose。任何一条没做到注册表都会在开发环境直接抛错而不是让页面安静地白屏把问题拖到交付之后才发现。万村乐数字乡村的这套注册表跑了两年零四个月大屏从九块长到四十七块组件族从两个扩到五个渲染入口那个文件的行数从八百多行降到一百四十行而且绝大多数改动发生在组件模块里。回头看注册表换来的不是代码更短而是让新增一类渲染引擎变成一件可以单独交付、单独回归的事也让展厅现场那个“到底哪里坏了”的问题有了一个明确的答案。