零依赖架构实战:451个HTML文件如何用63KB平均体积交付3584个交互模块
当你的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 MB | Measure-Object -Property Length -Sum |
| 平均文件体积 | 63.3 KB | 27.7MB / 448 |
| 最小文件 | 24.1 KB | 排序取最小 |
| 最大文件 | 146.9 KB | 排序取最大 |
| 外部依赖 | 0 | 全文无import、require、<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 元素的stroke和fill属性无法引用 CSS 变量(stroke:var(--teal)在 SVG attribute 中不生效)。每个arrow()调用需要直接传入颜色字符串,C.teal就是那个字符串。
这套设计的连锁效应是:修改一个颜色值,451 个文件的所有模块同步变化。但前提是——你必须手动同步:root和C。这是零依赖的代价:一致性靠纪律,不靠框架。
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根元素上。看起来没问题——但每次调用drawModule,clearNode(svg)会删除所有子元素,而gridBg在svg上累积的线条在清除后不会完全消失。原因是 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的设计强制每个模块函数drawM1到drawM8必须接受(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 动画类(apulse、aglow、adash、ablink、aspin)覆盖了基础动画需求,但无法实现物理引擎、粒子系统或复杂时间轴。这些场景 GSAP 或 D3 是正确的选择。
文件体积在增长。早期文件约 24KB,最新文件已达 146KB。随着模块内容复杂度增加,单文件体积会继续增长。如果超过 200KB,加载性能可能受影响,届时需要考虑代码拆分——但这与"零依赖单文件"理念冲突。
结论
451 个零依赖 HTML 文件证明了一件事:在特定场景下——可视化展示、教育内容、技术文档——零依赖架构不仅是可行的,而且在可移植性、持久性和审计性上有不可替代的优势。
代价是真实的:你自己造轮子、自己维护一致性、自己处理边缘场景。但当你为一个 63KB 的文件添加一个模块,双击打开看到 24 个交互式可视化瞬间渲染完成时,你会理解为什么这个选择值得。
所有代码开源,合集仓库地址:GitHub - wangzifan396-wzf/TW: AI 可视化实验室集 · 176 个交互式项目 · 1408+ 模块 · 零外部依赖 · 纯 HTML/CSS/JS + SVG · 覆盖 AI/ML、CS 系统、计算理论、交叉学科全谱系 · GitHub