当你的 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 变量
css
: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
javascript
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 画布绘制背景网格:
javascript
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 是每次切换模块时的重绘入口:
javascript
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 生成。一个反复出现的语法错误暴露了模式:
javascript
// 正确
t.textContent=s;
// 子 Agent 系统性产出(错误)
t.textContent=s);
多了一个右括号。这不是随机错误------它在几十个文件中以完全相同的形式出现。原因是子 Agent 在生成 txt 函数时,将 return t; 的语义与 t.textContent=s; 混淆,在语句末尾残留了函数调用的闭合括号。
这个错误的检测方法值得记录。用 Node.js 的 vm.Script 做语法校验:
javascript
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 偶尔产出:
javascript
// 错误 ------ 缺少属性键名 '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 参数:
javascript
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) |
矩形容器 | CSS border-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) |
背景网格 | CSS background-image |
nodeBox(g,...) |
带标题的节点框 | React 组件 |
pbar(g,...) |
进度条 | CSS width 动画 |
rbox(g,...) |
指标卡片 | React Stat 组件 |
chip(g,...) |
标签芯片 | CSS border-radius |
以 radar 函数为例,它用 20 行 vanilla JS 实现了 ECharts 雷达图组件的核心功能------多边形网格、数据多边形、标签定位:
javascript
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 个可视化。这不是随意选择------这是约束驱动设计。
javascript
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。这迫使作者为每个概念准备三个不同的可视化角度:
javascript
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 作为导航门户。优势是集中管理、统一搜索、一站式浏览。最新 commit 2c2c642,包含 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 个交互式可视化瞬间渲染完成时,你会理解为什么这个选择值得。