起因
我想做一批历史人物的「人生模拟器」——用户扮演巴菲特、霍金、曼德拉,面对他们真实经历的关键决策节点,选择后看到真实结果。
技术选型时我刻意选了最保守的方案:纯 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+。