零依赖架构实战: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 全文无 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 变量

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 元素的 strokefill 属性无法引用 CSS 变量(stroke:var(--teal) 在 SVG attribute 中不生效)。每个 arrow() 调用需要直接传入颜色字符串,C.teal 就是那个字符串。

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

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 根元素上。看起来没问题------但每次调用 drawModuleclearNode(svg) 会删除所有子元素,而 gridBgsvg 上累积的线条在清除后不会完全消失。原因是 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 的设计强制每个模块函数 drawM1drawM8 必须接受 (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 动画类(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

相关推荐
名为沙丁鱼的猫7293 小时前
【审计日志组件实践复盘】前端工程化 + 后端DDD架构(AOP切面拦截日志) + MBG
前端·架构
大霞上仙3 小时前
trae solo模式demo--用例管理平台
人工智能
xqqxqxxq4 小时前
AI智能旅游规划系统 - 前端技术笔记
前端·笔记·旅游
2601_958352909 小时前
接上USB,焊上麦,通话瞬间安静——WX-0813如何用AI降噪+100dB消回音,把嘈杂通话变成“金子“般清晰
人工智能·算法·语音识别·硬件开发·语音模块·降噪消回音
Shockang9 小时前
Agentic AI 工程实战
人工智能
陈随易10 小时前
bm2,MoonBit实现的pm2替代品
前端·后端·程序员
To_OC10 小时前
从 0 到 1:Milvus + 大模型打造私人记忆知识库
人工智能·node.js·llm
ii_best10 小时前
更新!移动端开发软件按键安卓版&手机助手v5.1.0上线!本地AI识别全面解锁,脚本开发再升级
android·人工智能·ios·按键精灵
火山引擎开发者社区10 小时前
数据库问题不用再找专家,问 DBCopilot 就行 —— 一图看懂你的数据库 AI 副驾
人工智能
ms365copilot11 小时前
PPT新模型Claude Opus 5
人工智能·microsoft·powerpoint·copilot