零依赖架构实战:451个HTML文件如何用63KB平均体积交付3584个交互模块
2026/7/31 16:09:57 网站建设 项目流程

当你的node_modules目录膨胀到 500MB 时,我在用 63KB 的单个 HTML 文件交付 24 个交互式可视化。

这不是标题党。451 个自包含 HTML 文件,总计 27.7MB,包含 3584+ 个交互式 SVG 模块,10752+ 个可视化视图——每个文件零外部依赖,零 CDN 引用,零 npm 包。双击打开即用,离线可用,永久可用。

这篇文章不是介绍这些项目做了什么,而是拆解一个工程问题:当你选择零依赖时,你获得了什么,又必须付出什么代价?答案藏在 CSS 变量的蝴蝶效应、gridBg必须画在<g>上的血泪教训,以及子 Agent 系统性产出畸形语法的规律里。

图1:单个可视化实验室的运行时效果——暗色主题、8模块导航、SVG节点流程图、右侧信息面板,全部在63KB的HTML文件内渲染

零依赖架构的数学:27.7MB vs 500MB+

先看事实。以下数据来自实际文件系统统计:

指标数值来源
HTML 可视化文件数448 个Get-ChildItem *-viz-lab.html
每文件模块数8 个MODULES数组定义
每模块视图数3 个mViews数组
总可视化模块3584+448 × 8
总可视化视图10752+448 × 24
文件总体积27.7 MBMeasure-Object -Property Length -Sum
平均文件体积63.3 KB27.7MB / 448
最小文件24.1 KB排序取最小
最大文件146.9 KB排序取最大
外部依赖0全文无importrequire<script src>
独立 GitHub 仓库225 个已启用 GitHub Pages

一个典型的可视化实验室文件约 63KB,包含完整的暗色主题、8 个交互模块、24 个差异化视图、雷达图/节点图/流程图/进度条等多种 SVG 图形元素。作为对比,一个空白的 React + D3 项目,node_modules轻松突破 200MB。

零依赖的收益不止体积。可移植性:文件复制到任何地方都能打开,U盘、邮件附件、内网服务器。持久性:十年后这个 HTML 照样能跑,而 npm 生态的包可能早已废弃或 breaking change。审计性:63KB 的源码,一个下午就能通读全部逻辑。

代价是什么?你需要自己造所有轮子。

双轨色彩系统:一个 CSS 变量如何驱动 3584 个模块

零依赖意味着没有 Tailwind,没有 CSS-in-JS。451 个文件保持视觉一致性的核心,是一套"双轨色彩系统"。

第一轨:CSS:root变量

:root{ --bg:#0a0b0f;--bg2:#0f1117;--bg3:#161922;--bg4:#1c2030; --border:#1e2433;--border2:#2a3144; --text:#e2e8f0;--text2:#94a3b8;--text3:#64748b; --teal:#00d4aa;--cyan:#22d3ee;--amber:#fbbf24; --red:#ef4444;--purple:#a78bfa;--blue:#3b82f6; --pink:#f472b6;--green:#34d399; --mono:"'SF Mono','JetBrains Mono',Consolas,monospace"; }

18 个变量定义了整个设计系统。背景分 4 层(bg→bg4),文本分 3 级(text→text3),8 种语义色覆盖所有图表场景。这不是随意选的——每一层都有明确的用途:bg是画布底色,bg2是面板,bg3是卡片,bg4是内嵌元素。

第二轨:JavaScript 镜像对象C

var C={ bg:'#0a0b0f',bg2:'#0f1117',bg3:'#161922',bg4:'#1c2030', border:'#1e2433',border2:'#2a3144', text:'#e2e8f0',text2:'#94a3b8',text3:'#64748b', teal:'#00d4aa',cyan:'#22d3ee',amber:'#fbbf24', red:'#ef4444',purple:'#a78bfa',blue:'#3b82f6', pink:'#f472b6',green:'#34d399', mono:"'SF Mono','JetBrains Mono',Consolas,monospace" };

C对象与:root一一对应。为什么需要两套?因为 SVG 元素的strokefill属性无法引用 CSS 变量(stroke:var(--teal)在 SVG attribute 中不生效)。每个arrow()调用需要直接传入颜色字符串,C.teal就是那个字符串。

这套设计的连锁效应是:修改一个颜色值,451 个文件的所有模块同步变化。但前提是——你必须手动同步:rootC。这是零依赖的代价:一致性靠纪律,不靠框架。

gridBg 的血泪教训:为什么必须画在<g>

这是整个项目中代价最高的一堂课。

gridBg函数为每个 SVG 画布绘制背景网格:

function gridBg(g,w,h){ for(var x=0;x<=w;x+=45){ E(g,'line',{x1:x,y1:0,x2:x,y2:h,stroke:'#141926','stroke-width':1}); } for(var y=0;y<=h;y+=45){ E(g,'line',{x1:0,y1:y,x2:w,y:y,stroke:'#141926','stroke-width':1}); } E(g,'rect',{x:0,y:0,width:w,height:h,fill:'none', stroke:'#1e2433','stroke-width':1.5,rx:4}); }

问题出在调用位置。drawModule是每次切换模块时的重绘入口:

function drawModule(mi){ mCur=mi; var svg=$('svg'); clearNode(svg); // 清空 SVG 所有子元素 var g=E(svg,'g',{id:'layer'}); // 创建新的 g 层 gridBg(g,900,480); // 在 g 上画网格 —— 正确 buildNav(); buildTabs(mi); var fns=[drawM1,drawM2,drawM3,drawM4,drawM5,drawM6,drawM7,drawM8]; fns[mi](g,mViews[mi]); }

关键在第 4-5 行:先clearNode(svg)清空画布,再创建新的<g>元素,最后在<g>上画网格。

最初版本把gridBg直接画在svg根元素上。看起来没问题——但每次调用drawModuleclearNode(svg)会删除所有子元素,而gridBgsvg上累积的线条在清除后不会完全消失。原因是 SVG 命名空间的元素在直接附加到根svg时,某些浏览器会缓存渲染层。网格线会一层层叠加,最终变成纯黑矩形,遮盖所有内容。

修复方法就是你现在看到的:永远在新建的<g>元素上画网格。clearNode删除旧<g>,新建<g>是干净的,网格只画一次。

这个 bug 影响了早期几十个文件。教训:SVG 操作的副作用与 DOM 操作不同,命名空间元素的清除和重建有微妙差异。

子 Agent 的系统性错误:.textContent=s)不是笔误

在批量创建 451 个文件的过程中,大量代码由 AI 子 Agent 生成。一个反复出现的语法错误暴露了模式:

// 正确 t.textContent=s; // 子 Agent 系统性产出(错误) t.textContent=s);

多了一个右括号。这不是随机错误——它在几十个文件中以完全相同的形式出现。原因是子 Agent 在生成txt函数时,将return t;的语义与t.textContent=s;混淆,在语句末尾残留了函数调用的闭合括号。

这个错误的检测方法值得记录。用 Node.js 的vm.Script做语法校验:

var vm = require('vm'); var js = html.match(/<script>([\s\S]*?)<\/script>/)[1]; try { new vm.Script(js); console.log('OK'); } catch(e) { console.log('FAIL: ' + e.message); }

vm.Script提供精确的错误行号和列号,远优于渐进式编译。批量验证脚本在几秒内扫描 448 个文件,定位每个语法错误的确切位置。

另一个系统性错误同样有规律:circle元素缺少r属性。子 Agent 偶尔产出:

// 错误 —— 缺少属性键名 'r' circ(g,cx,cy,sz/2+4,{stroke:C.teal}); // 正确 —— 'r' 显式声明 circ(g,cx,cy,sz/2+4,{r:sz/2+4,stroke:C.teal});

修复后,circ函数本身被加固,强制要求r参数:

function circ(g,cx,cy,r,opts){ opts=opts||{}; var a={cx:cx,cy:cy,r:r,fill:opts.fill||'none', stroke:opts.stroke||C.teal,'stroke-width':opts.sw||1.5}; if(opts.cls)a['class']=opts.cls; E(g,'circle',a); }

教训:当用 AI 批量生成代码时,系统性错误会以固定模式重复出现。人类笔误是随机的,AI 错误是结构性的。对付结构性错误,最有效的方法不是逐个修复,而是加固底层函数,让错误在结构上无法发生。

15 个核心函数:替代整个前端框架

零依赖的代价是自己造轮子。但轮子的数量比你想象的少——15 个核心函数覆盖了所有可视化需求:

函数用途替代品
E(parent,tag,attrs)创建 SVG 元素document.createElementNS封装
$(id)获取 DOM 元素document.getElementById
clearNode(n)清空子元素while(n.firstChild)
box(g,x,y,w,h)矩形容器CSSborder-box
txt(g,x,y,s)文本标签HTML<span>
line(g,x1,y1,x2,y2)直线SVG<line>
arrow(g,...)带箭头连线D3 的linkHorizontal
circ(g,cx,cy,r)圆形D3 的circle生成器
poly(g,pts)多边形D3 的polygon
radar(g,...)雷达图ECharts radar 组件
gridBg(g,w,h)背景网格CSSbackground-image
nodeBox(g,...)带标题的节点框React 组件
pbar(g,...)进度条CSSwidth动画
rbox(g,...)指标卡片React Stat 组件
chip(g,...)标签芯片CSSborder-radius

radar函数为例,它用 20 行 vanilla JS 实现了 ECharts 雷达图组件的核心功能——多边形网格、数据多边形、标签定位:

function radar(g,cx,cy,r,labels,data,opts){ opts=opts||{}; var n=labels.length,i,a,rr; // 画 4 层同心多边形网格 for(var ring=1;ring<=4;ring++){ var pts=[]; for(i=0;i<n;i++){ a=-Math.PI/2+i*2*Math.PI/n; rr=r*ring/4; pts.push((cx+Math.cos(a)*rr).toFixed(1)+','+ (cy+Math.sin(a)*rr).toFixed(1)); } E(g,'polygon',{points:pts.join(' '),fill:'none', stroke:C.border,'stroke-width':1}); } // 画轴线 for(i=0;i<n;i++){ a=-Math.PI/2+i*2*Math.PI/n; E(g,'line',{x1:cx,y1:cy, x2:cx+Math.cos(a)*r,y2:cy+Math.sin(a)*r, stroke:C.border,'stroke-width':1}); } // 画数据多边形 var dpts=[]; for(i=0;i<n;i++){ a=-Math.PI/2+i*2*Math.PI/n; rr=r*data[i]/100; dpts.push((cx+Math.cos(a)*rr).toFixed(1)+','+ (cy+Math.sin(a)*rr).toFixed(1)); } E(g,'polygon',{points:dpts.join(' '), fill:opts.fill||'rgba(0,212,170,0.15)', stroke:opts.stroke||C.teal,'stroke-width':2}); // 画标签 for(i=0;i<n;i++){ a=-Math.PI/2+i*2*Math.PI/n; var lx=cx+Math.cos(a)*(r+22), ly=cy+Math.sin(a)*(r+22)+4; txt(g,lx,ly,labels[i],{fs:10,fill:C.text2,ta:'middle'}); } }

ECharts 的雷达图组件源码超过 2000 行。不是 ECharts 臃肿——它处理了动画、tooltip、响应式、主题切换等大量边缘场景。但如果你只需要静态渲染一个数据多边形加标签,20 行就够了。

这就是零依赖架构的核心权衡:你放弃的是通用性和边缘场景处理,获得的是极简、可控和永恒。

模块系统:8 × 3 = 24 的约束设计

每个文件包含 8 个模块,每个模块 3 个视图,共 24 个可视化。这不是随意选择——这是约束驱动设计。

var MODULES=[ {n:'M1',t:'概览',d:'项目全景',v:['全景图','核心指标','定位分析']}, {n:'M2',t:'架构',d:'技术架构',v:['架构图','模块分解','数据流']}, // ... M3-M8 ]; var mCur=0,mViews=[0,0,0,0,0,0,0,0],mLayers=[];

mViews是一个长度为 8 的数组,记录每个模块当前显示的视图索引。切换模块时drawModule(mi)重绘整个 SVG,切换视图时只改变mViews[mi]的值。

约束在哪里?drawModule的设计强制每个模块函数drawM1drawM8必须接受(g, vi)两个参数,其中vi是 0、1 或 2。这迫使作者为每个概念准备三个不同的可视化角度:

function drawM1(g,vi){ var side=$('mpSide'); clearNode(side); if(vi===0){ // 视图1:全景架构图 —— nodeBox + arrow 流程 }else if(vi===1){ // 视图2:核心指标 —— rbox 数字卡片 + radar 雷达图 }else{ // 视图3:定位分析 —— 对比表格 + pbar 进度条 } }

8 模块的约束来自认知负荷理论:7±2 是人类工作记忆的容量上限。8 个模块恰好在上限附近,迫使作者提炼核心维度,而不是堆砌信息。3 个视图的约束来自"同一概念的三种表达"方法论——架构图、数据指标、对比分析,覆盖空间、数值和关系三个维度。

部署架构:一个合集 + 225 个独立仓库

451 个文件有两种部署路径,互为补充:

合集仓库(TW-main):所有 HTML 文件放在一个仓库,一个index.html作为导航门户。优势是集中管理、统一搜索、一站式浏览。最新 commit2c2c642,包含 481 个 HTML 文件(含可视化实验室 + 工具页面)。

独立仓库(225 个):每个可视化实验室复制为独立仓库的index.html,启用 GitHub Pages。优势是每个项目有独立 URL,利于搜索引擎索引和社交分享。例如superpowers-agent-skills-framework-viz-lab仓库对应https://wangzifan396-wzf.github.io/superpowers-agent-skills-framework-viz-lab/

双轨部署的代价是同步开销——每次新增项目需要更新合集仓库的index.html(新增卡片 + 更新统计数字),然后运行 PowerShell 脚本批量创建独立仓库。但收益是搜索曝光最大化:合集仓库获取聚合流量,独立仓库获取长尾关键词流量。

局限性

零依赖架构不是银弹。以下是我明确承认的局限:

不适用于复杂状态管理。当应用状态超过mViews数组的复杂度,需要路由、Store、副作用管理时,零依赖方案的维护成本会指数级上升。451 个文件之所以可行,是因为每个文件的状态模型完全相同。

不适用于团队协作。15 个核心函数没有类型系统、没有 lint 规则、没有模块边界。一个人维护没问题,5 个人同时改就会冲突。TypeScript + ESLint + 构建工具的存在是有理由的。

不适用于需要动画引擎的场景。CSS 动画类(apulseaglowadashablinkaspin)覆盖了基础动画需求,但无法实现物理引擎、粒子系统或复杂时间轴。这些场景 GSAP 或 D3 是正确的选择。

文件体积在增长。早期文件约 24KB,最新文件已达 146KB。随着模块内容复杂度增加,单文件体积会继续增长。如果超过 200KB,加载性能可能受影响,届时需要考虑代码拆分——但这与"零依赖单文件"理念冲突。

结论

451 个零依赖 HTML 文件证明了一件事:在特定场景下——可视化展示、教育内容、技术文档——零依赖架构不仅是可行的,而且在可移植性、持久性和审计性上有不可替代的优势。

代价是真实的:你自己造轮子、自己维护一致性、自己处理边缘场景。但当你为一个 63KB 的文件添加一个模块,双击打开看到 24 个交互式可视化瞬间渲染完成时,你会理解为什么这个选择值得。

所有代码开源,合集仓库地址:GitHub - wangzifan396-wzf/TW: AI 可视化实验室集 · 176 个交互式项目 · 1408+ 模块 · 零外部依赖 · 纯 HTML/CSS/JS + SVG · 覆盖 AI/ML、CS 系统、计算理论、交叉学科全谱系 · GitHub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询