用原生 JS 做了一个中国历史帝皇梳理的可视化站(7 朝 77 帝 + Leaflet 地图)

2 阅读5分钟

用原生 JS 做了一个中国历史可视化站(7 朝 77 帝 + Leaflet 地图)

仓库:github.com/zinging/chi…

在线演示:zinging.github.io/chinese-his…

觉得不错希望能点个★支持一下。

技术栈

技术架构:

  • 前端:原生 HTML + CSS + JavaScript,没框架,没构建
  • 地图:Leaflet 1.9.4,vendor 到本地,不走 CDN
  • 数据:全是 JSON,按朝代分目录
  • 后端:没有后端,一个 30 行的 Node 静态服务器
  • 部署:GitHub Pages / Gitee Pages,纯静态
  • 头像:AI 生成工笔风,512×512

这个项目交互不复杂 —— 卡片切换、地图缩放、悬案展开、选择题作答。上 React 要多写一层构建和状态管理,不值当。原生 JS 一个 app.js 1900 行,能 hold 住。

项目结构

chinese-history-atlas/
├── data/                  # 所有数据都是 JSON
│   ├── sui/ tang/ wudai/ song/ yuan/ ming/ qing/
│   │   ├── emperors/      # 皇帝卡片(生平、时间线、人物、冷知识)
│   │   ├── cases/         # 悬案档案
│   │   ├── places/        # 古地名(含今地名对照、经纬度)
│   │   ├── routes/        # 行军/航海路线
│   │   ├── quiz.json      # 史论选择题
│   │   └── people.json    # 关键人物小传
│   ├── boundaries/        # 各朝疆域 GeoJSON
│   └── portraits/         # 头像
├── web/
│   ├── index.html
│   ├── app.js
│   ├── style.css
│   ├── serve.mjs          # 静态服务器
│   └── vendor/leaflet/   # Leaflet 本地化
└── scripts/

数据层和展示层完全分离。要加一个朝代,新建一个目录写 JSON,前端不用动。

我的技术决策

1. Leaflet 为什么本地化?

一开始走 CDN,后来发现两个问题:国内访问偶尔慢,CDN 挂了地图直接白屏。下载到 web/vendor/ 本地引用,零外部依赖,部署到哪都能跑。

2. 人物关系图用 SVG 画

皇帝在左侧,关键人物(皇后、大臣、对手)右侧竖排,虚线连接。关系标签(比如 "尚书左仆射・宰相")是动态宽度的圆角矩形。最开始写死 84px,长职位名直接溢出,改成根据文字长度自适应:

const rel = p.relation || p.role || '';
const w = Math.max(56, rel.length * 15 + 14);
return `<rect x="${mx - w/2}" y="${my-13}" width="${w}" height="24" rx="12" .../>`;

不复杂,但比写死尺寸省事。

3. 疆域是手绘 GeoJSON

各朝疆域轮廓不是扒的数据源,是对着谭其骧《中国历史地图集》手绘的简化多边形,标注了 "示意,非精确边界"。

这里踩过一个坑:隋唐元明清的疆域轮廓一开始画成一样的,被用户指出来才重新对着地图改。疆域这种东西,差一点就是错的。

郑和七下西洋、靖难之役朱棣南下、清军入关,路线都在 routes.json 里,用 Leaflet polyline 分段动画画出来。

4. 380 道选择题没用框架

史论工坊的数据在 quiz.json,前端渲染选项按钮、判断对错、展示解析。选项全用按钮胶囊,不用 <select>,状态全在内存,刷新重置。

题分两类:一类是论证判断("隋炀帝开大运河,功在千秋还是罪在当代?"),一类是因果链("安史之乱为什么爆发?")。约四分之一是开放题,可以接自己的 OpenAI 兼容接口做 AI 点评,配置存本地,不推仓库。

5. 静态服务器自己写

前端 fetch('../data/*.json') 读数据,双击 HTML 会被 CORS 拦。30 行 Node 解决:

import { createServer } from 'http';
import { readFile } from 'fs/promises';
import { extname, join } from 'path';

const types = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css',
  '.json':'application/json', '.png':'image/png', '.gif':'image/gif' };

createServer(async (req, res) => {
  const p = join(process.cwd(), req.url === '/' ? '/web/index.html' : req.url);
  try {
    const buf = await readFile(p);
    res.setHeader('Content-Type', types[extname(p)] || 'application/octet-stream');
    res.end(buf);
  } catch { res.statusCode = 404; res.end('not found'); }
}).listen(5173, () => console.log('http://127.0.0.1:5173/web/'));

Node 18+ 自带 fetch 和 createServer,不需要 npm install。

6. 五代十国怎么展示

这是个设计问题。五代(后梁→后唐→后晋→后汉→后周)是先后衔接的,可以按时间线推进;十国是同时并存的,最后用了横向甘特图 —— 每个政权一条横条,谁先建国谁后被灭,一目了然。

数据量

  • 7 个朝代:隋、唐、五代十国、宋、元、明、清
  • 77 位皇帝,每位有生平时间线、人物关系、冷知识、争议点
  • 380 道史论题
  • 545 条时间线
  • 224 张 AI 生成头像

为什么三十岁了才对历史感兴趣

三十岁了才开始对历史感兴趣,正如唐太宗说的“以铜为镜可以正衣冠,以古为镜可以知兴替,以人为镜可以明得失”

我觉得是在学事情是怎么一步步走到这一步的。所以我就想让ai做一个前端网站帮我梳理,也能干中学。

杨广修了大运河,搞了科举,打了西域,建了洛阳 —— 每件事单拎出来都是好事。但他把所有事挤在同一个十年里干完,老百姓一年征发两百万,然后国家没了。你从这里学到的不是 "杨广是暴君",是好事做太急也会死人。

柴荣在位五年半,西败后蜀,三征南唐,北伐契丹,三十九岁病死。然后赵匡胤陈桥兵变,捡了个满配存档。你从这里学到的不是 "赵匡胤天纵神武",是你看到的结果,往往是别人铺好的路。

万历三十年不上朝,但万历三大征一场没落下,奏折批得比谁都勤。你从这里学到的不是 "万历摆烂",是人是复杂的,别用一个词概括任何人。

历史不会重复,但人会。一千年以前的人面对的问题 —— 权力怎么分配、功劳大了怎么收场、改革太急会不会翻车、信任的人会不会背叛 —— 和今天没有本质区别。你把这些故事看够了,再看身边的事,会有一种 "这个剧本我读过" 的感觉。

还有一点。历史看多了你会发现,很多我们以为必然发生的事,在当时的人眼里全是未知数。李建成那天早上不知道玄武门有埋伏,柴荣不知道自己三十九岁会死,崇祯到最后都觉得还有机会。结果是确定的,过程从来不是。 这会让你对自己正在做的事多一点耐心 —— 你也在过程里,你不知道结局。

所以学历史有什么用?

它不会让你升职加薪,不会让你少写 bug。但它会让你知道,你遇到的事,一千年前有人遇到过,一千年后还会有人遇到。你不是第一个,也不是最后一个。

这个认知本身,就是一种安慰。

在线体验

手机也能看,默认打开是隋朝,顶部切朝代。