不用框架不用后端,一个 HTML 文件做一个完整的交互应用——我做了 100 个

2 阅读3分钟

起因

我想做一批历史人物的「人生模拟器」——用户扮演巴菲特、霍金、曼德拉,面对他们真实经历的关键决策节点,选择后看到真实结果。

技术选型时我刻意选了最保守的方案:纯 HTML + 原生 JS,零依赖,零后端,单文件。

原因很简单:我把这批工具部署在一个静态目录下,不想维护服务器、不想管构建流程、不想处理依赖升级。单文件丢上去就能用,10 年后打开还能用。

最终做了 100 个,每个平均文件大小 80KB 左右。这是我在这个过程里踩到的一些技术细节。


核心数据结构

每个模拟器本质上是一棵决策树。我用 JS 对象描述:

  const STORY = {
    start: {
      year: 1930,
      age: 0,
      text: "Warren Buffett was born in Omaha, Nebraska. His father Howard was a stockbroker...",
      choices: [
        {
          label: "Follow your father into stocks early",
          outcome: "early_investor",
          score: 10
        },
        {
          label: "Focus on school, delay investing",
          outcome: "scholar_path",
          score: 5
        }
      ]
    },
    early_investor: {
      year: 1941,
      age: 11,
      text: "At 11, you buy your first stock — Cities Service Preferred at $38...",
      choices: [
        {
          label: "Hold through the dip",
          outcome: "patience_learned",
          score: 20,
          historical: true   // 标记这是历史上真实的选择
        },
        {
          label: "Sell when it drops to $27",
          outcome: "early_lesson",
          score: 8
        }
      ]
    }
    // ...更多节点
  };


  historical: true 标记真实历史选择,结尾统计用户选择了多少次和历史人物一致。

  ---
  状态管理——不用框架怎么做

  不用 React/Vue,状态就是几个变量:

  let currentNode = 'start';
  let score = 0;
  let matchCount = 0;   // 和历史人物决策相同的次数
  let totalChoices = 0;
  const history = [];   // 决策历史,用于「回放」功能

  function makeChoice(outcomeKey, choiceScore, isHistorical) {
    history.push({ node: currentNode, choice: outcomeKey });
    score += choiceScore;
    totalChoices++;
    if (isHistorical) matchCount++;

    currentNode = outcomeKey;
    render();
  }

  function render() {
    const node = STORY[currentNode];
    if (!node) { showResult(); return; }

    document.getElementById('year').textContent = node.year;
    document.getElementById('story-text').textContent = node.text;

    const choicesEl = document.getElementById('choices');
    choicesEl.innerHTML = '';
    node.choices.forEach(choice => {
      const btn = document.createElement('button');
      btn.textContent = choice.label;
      btn.onclick = () => makeChoice(choice.outcome, choice.score, choice.historical);
      choicesEl.appendChild(btn);
    });
  }

没有虚拟 DOM,没有响应式,直接操作 DOM。100 个节点的决策树,这个方式完全够用,性能也没有任何问题。


进度持久化——用 localStorage

用户刷新页面不丢失进度:

  function saveProgress() {
    localStorage.setItem('sim_progress', JSON.stringify({
      node: currentNode,
      score,
      matchCount,
      totalChoices,
      history
    }));
  }

  function loadProgress() {
    const saved = localStorage.getItem('sim_progress');
    if (!saved) return false;
    const data = JSON.parse(saved);
    currentNode = data.node;
    score = data.score;
    matchCount = data.matchCount;
    totalChoices = data.totalChoices;
    history.length = 0;
    history.push(...data.history);
    return true;
  }

  // 每次决策后保存
  function makeChoice(outcomeKey, choiceScore, isHistorical) {
    // ...原来的逻辑
    saveProgress();
    render();
  }

100 个文件怎么管——用 AI 批量生成框架

每个模拟器的 HTML 结构完全相同,差异只在 STORY 数据和人物名字。我用 AI(Claude)帮我生成每个人物的决策树内容,然后套进同一个模板。

伪代码:

模板 = 读取 simulator-template.html for 每个人物 in 人物列表: story_data = AI生成(人物名 + 关键生平事件) 输出文件 = 模板.replace('{{STORY_DATA}}', story_data) 写入 f"{人物名}-simulator.html"

AI 生成的内容需要人工核对历史准确性——这是不能省的步骤。但结构生成、格式统一、文案润色这些可以完全交给 AI。


单文件的局限

老实说也有代价:

  • 代码复用差:100 个文件里有很多重复的 CSS 和 JS,改一个样式要改 100 个文件
  • 没有组件化:想加一个新功能(比如「分享结果」按钮),要手动改 100 个文件,或者写脚本批量替换
  • 调试不方便:没有 source map,没有热更新

我的取舍标准是:这批工具内容更新频率极低,一旦做好基本不需要改。用框架引入的复杂度(构建流程、依赖管理、部署配置)对我来说成本更高。

如果是需要频繁迭代的产品,这套方案不适合。


最终效果

100 个模拟器,涵盖科学家、企业家、艺术家、政治家,全部部署在: 👉 ordinarymantrying.com/tools/simul…

单页加载时间 < 1s(无外部依赖),Google PageSpeed 移动端 90+。