为什么浏览器"另存为"总是失败?kage"先渲染、再剥离、后本地化"三阶段原理全解
【免费下载链接】kageShadow any website for offline viewing, with the JavaScript stripped out项目地址: https://gitcode.com/gh_mirrors/kage6/kage
你有没有这样的经历:想永久保存一篇文章,点了浏览器"另存为网页",半年后打开却只剩一片空白或永远转圈的加载图标。kage 就是为解决这个问题而生的开源离线存档工具:它克隆网站供离线浏览,并把所有 JavaScript 剥离干净。本文将拆解 kage "先渲染、再剥离、后本地化"的三阶段原理,帮你一次看懂它为什么比"另存为"可靠得多。
浏览器"另存为"到底败在哪?
问题不在"存",而在"存错了东西"。现代网页早就不是静态文档,而是一个靠 JavaScript 实时生成的应用,"另存为"通常会在三个地方翻车:
| 失败模式 | 表现 | 根本原因 |
|---|---|---|
| 空白页 | 打开后只剩骨架 | 保存的是"图纸"而非"成品",JS 没跑,内容由 JS 生成 |
| 死链图片 | 图片裂开、样式丢失 | CSS 和图片仍指向远程服务器,服务器一改或一关就失效 |
| 幽灵请求 | 离线时仍在偷偷联网 | 保存的 HTML 里还带着统计、推送、埋点脚本 |
一句话总结:"另存为"存的是别人的程序,而不是你看到的内容。页面从未真正属于你,它只是别人 JavaScript 的一个瘦客户端。
kage 的三阶段流水线
kage 走了另一条路:先用真实的浏览器把页面"看完",再撕掉所有脚本,最后把资源搬到本地。整条管线可以概括为一条流水线:
种子 URL → 无头 Chrome 渲染 → 最终 DOM 快照 → 剥离 JS → 本地化资源 → 落盘
下面逐阶段拆解。
阶段一:先渲染——让无头 Chrome 替你"看"完页面
kage 不是抓取 HTML 源码,而是驱动一个真实的无头 Chrome 打开页面,就像你亲眼去看它一样。核心逻辑在 browser/pool.go:
- 等页面"安静"下来:加载完成后还会等待约 1.5 秒的网络空闲期(
Settle),让 AJAX 数据、字体、图片全部到位,再对最终 DOM 拍快照——保存的就是人眼实际看到的那个页面; - 自动滚动:加
--scroll参数后,kage 会模拟滚动整页,触发懒加载图片与无限流内容,再滚回顶部快照; - 有并发、有兜底:默认 4 个页面并行渲染(
Workers),每页有 30 秒渲染超时,单页出错不会拖垮整场克隆; - 安全优先:Chrome 沙箱默认开启,禁止浏览器私自下载文件到磁盘,所有下载都走 kage 自己的下载器。
这一步解决的是"另存为"的死穴:内容必须先在浏览器里"活"过一次,才能被完整保存。
阶段二:再剥离——把"活页面"变成"死标本"
拿到渲染好的最终 DOM 后,kage 交给 sanitize/sanitize.go 做外科手术式清理。它用标准 HTML 解析器遍历整棵 DOM 树,删掉一切会动、会联网的东西:
- 📄 所有
<script>标签; - ✋ 所有
onclick、onload等事件处理器属性; - 🔗 一切
javascript:伪协议链接(替换为#); - ⏳
<meta http-equiv="refresh">自动跳转; - 🚪
preconnect、dns-prefetch等离线毫无意义的预连接提示; - 甚至包括藏在 IE 条件注释里、能绕过普通遍历的
<script>。
同时它保留样式、图片、字体、表单和全部语义标签——布局原封不动。官方对此有一句绝妙的比喻:保存后的页面是一张照片,而不是一个程序。没有跟踪、没有网络调用、没有意外,就是一堆能直接双击打开的.html文件。
阶段三:后本地化——让每个链接都指向你的硬盘
前两阶段解决"内容完整"和"绝对安全",第三阶段解决"离线可用"。这一步由两个模块协作完成:
1. 确定性 URL→路径映射(urlx/urlx.go)
每个 URL 都会经过Normalize归一化(小写化 scheme/host、去 fragment、去默认端口),然后映射到一个确定的本地路径。妙处在于这是一个纯函数:不碰网络、不碰磁盘,所以页面工人可以在资源还没下载完之前,就先把它在 HTML 里的链接改写成本地相对路径——两边提前约定好"字节将来放在哪",天然支持并行。
2. 资源下载与改写(asset/)
页面里的<img src>、<a href>被asset.RewriteHTML批量改写;CSS 里的url(...)和@import则由 asset/css.go 中的RewriteCSS处理,指向的字体和图片同样拉回本地。资源走的是独立的普通 HTTP 下载池,与浏览器渲染互不阻塞。
整个过程的编排代码在 clone/cloner.go:先渲染 → 改写链接 → 剥离脚本 → 写入磁盘,一气呵成。
三阶段跑完,磁盘上长什么样?
镜像目录一目了然:
paulgraham.com/ ├── index.html # 首页,脚本已剥离 ├── greatwork.html # 某篇文章 └── _kage/ # 保留区:资源与爬取状态 ├── paulgraham.com/site.css # 本地化样式(url() 已改写) ├── paulgraham.com/pg.png └── state.json # 已访问集合,支持断点续爬所有资源都躺在_kage/里,所有链接都已是相对路径。接下来怎么用,完全由你决定:
kage serve 目录—— 起个本地静态服务器,在浏览器里预览;kage pack 镜像目录—— 压缩成单个.zim归档,或打包成一个自带网站的可执行文件,发给别人什么都不用装;- 用 webview 标签构建后,打开就是一个原生窗口里的离线站点,像真正的桌面应用。
与"另存为"的对比:思维方式变了
| 浏览器"另存为" | kage | |
|---|---|---|
| 保存对象 | 未渲染的 HTML 源码 + 部分附件 | 渲染后的最终 DOM 快照 |
| JavaScript | 原样保留,离线仍可能联网 | 全部剥离,页面是"照片"不是"程序" |
| 资源链接 | 指向远程服务器 | 全部改写为本地相对路径 |
| SPA / 懒加载页面 | 空白或残缺 | 完整呈现,可自动滚动触发加载 |
| 断点续传 | 无 | Ctrl-C 保存进度,下次接着跑 |
结语
回到开头的问题:浏览器"另存为"失败,是因为它试图保存一个永远在别人的服务器上运行的页面。而 kage 的三阶段原理给出了另一种答案——
📸先渲染,让你看到的成为你拥有的;✂️再剥离,让拥有变为安全;📦后本地化,让安全变为可随身携带。
这三步走完后,落盘的就不是一份网页副本,而是一份可以离线阅读十年、丢给任何设备都能打开的"网站快照"。想深入了解配置细节,可以查阅项目自带的文档 docs/ 与快速上手指南 docs/content/getting-started/quick-start.md。
【免费下载链接】kageShadow any website for offline viewing, with the JavaScript stripped out项目地址: https://gitcode.com/gh_mirrors/kage6/kage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考