我逆向工程了 Framer 的 React 运行时,以将站点导出为 static HTML

发布: (2026年3月15日 GMT+8 15:17)
9 分钟阅读
原文: Dev.to

Source: Dev.to

我有一个 Framer 站点(nocodetalks.co),已经运行了 2 年,支付 $10 / month 用于静态站点托管。没有动态内容、没有 CMS、没有表单——只有 Framer 服务器上的普通 HTML。

当我尝试将它迁移到 Vercel(免费层,性能相同)时,遇到了阻碍:Framer 没有代码导出。他们的帮助中心直截了当地说明——你可以在 Framer 上构建,但不能把文件导出并离开

我和几位处于相同情况的朋友聊过。他们都想迁移到 Vercel 或 Cloudflare Pages,却找不到导出代码的办法。

于是我自己写了一个工具来实现这个功能。最初只是为我的站点写的脚本,后来演变成了一个产品。下面是我对 Framer 底层工作原理的了解,以及为什么“仅保存 HTML”不可行的原因。

为什么 “View Source” 不起作用

Framer 网站 看起来 像普通的 HTML,当你右键 → View Source 时,但它们实际上是 React 应用。

  1. 服务器发送 pre‑rendered HTML
  2. 客户端随后加载一个 JavaScript 包,调用 hydrateRoot() 接管 DOM。

如果你保存 HTML 文件并在本地打开:

  • React 包会尝试从 Framer 的 CDN 加载。
  • Hydration 会运行,但 API 调用会失败,因为你不在 Framer 的域名下。
  • React 要么抛出错误,要么清空 DOM。

结果:空白页面或布局损坏。

View Source 中看到的 HTML 仅是 initial server render。真实的站点运行在 React 的运行时中。

我必须解决的5个问题

1. 去除 React 而不破坏页面

爬虫首先捕获每个页面的 HTML,然后 移除所有与 React hydration 相关的 <script> 标签(主入口、modulepreload 提示、内联 hydrateRoot 调用)。

Framer 还会为交互组件(FAQ 手风琴、移动导航菜单、标签切换器)注入脚本。删除 所有 JavaScript 会导致这些组件以错误的方式变成静态。

解决方案

  • 去除 React/hydration 层。
  • 注入小型 vanilla‑JS 脚本来复现交互行为(例如,一个 20 行的手风琴脚本、一个 15 行的移动菜单切换脚本)。
  • 这样可以用几百字节的自定义代码取代数百千字节的 React 运行时。

2. 隐形内容(滚动动画)

Framer 通过将 opacity: 0 设置为隐藏应在滚动时出现的元素。在真实站点中,Framer 的 JavaScript 会检测滚动位置并将它们淡入。而在导出的版本中,这些脚本已不存在,导致元素保持不可见。

解决方案

  • 爬虫 自动从顶部滚动到页面底部,并在间隔处暂停以让图片加载。
  • 滚动完成后,等待 DOM 稳定(500 ms 内没有新的变动)再捕获最终的 HTML。
  • 这会在保存 HTML 之前强制元素变为可见。

5. CSS url() 引用

Framer 的样式表使用 绝对 URL 引用字体和背景图像,这些 URL 指向 Framer 的 CDN(例如 https://framerusercontent.com/...)。

解决方案

  1. 下载 CSS url() 声明中引用的每个资源。
  2. 将它们保存到本地。
  3. 将 URL 重写为 相对路径

该过程是递归的:某些 CSS 文件会 @import 其他 CSS 文件,这些文件又引用字体,进而引用更多资源。爬虫会沿着链条一直追踪,直至所有资源本地化。

架构

组件角色
Puppeteer用于渲染页面和执行 JavaScript 的无头 Chrome
Cheerio捕获后解析并重写 HTML
Regex重写 CSS url() 路径(Cheerio 不解析 CSS)
ExpressAPI 服务器和预览 UI
p‑queue并发控制(最多 2 个浏览器实例)
archiver流式 ZIP 创建用于下载
  • 爬取策略: 广度优先搜索(BFS)。从主页开始,提取所有内部链接,逐个访问,循环。每次导出上限为 50 页
  • 资源下载: 8 个并发 https.get 请求。直接的 HTTP 请求比使用 Puppeteer 下载资源快约 10 倍。
  • 代码规模: 爬虫约 1,100 行,URL 重写器约 400 行,ZIP 打包器约 200 行。

输出示例

一个典型的导出 Framer 网站会提供如下的文件夹结构:

index.html
about.html
pricing.html
contact.html
assets/
  css/
    a3f2c1_styles.css
    b7e9d4_chunk.css
  js/
    menu-toggle.js
    faq-accordion.js
    scroll-reveal.js
  images/
    hero.webp
    team-photo.jpg
    logo.svg
  fonts/
    inter-var.woff2
    playfair-display.woff2
manifest.json

每个 HTML 文件都是 自包含 的,使用相对路径,随时可以部署到任何静态托管服务商(Vercel、Cloudflare Pages、Netlify 等)。

TL;DR

  • Framer 站点是 React 应用;不能仅仅“保存 HTML”。
  • 去除 React、处理滚动显现、悬停效果、懒加载图片以及 CDN 引用的资源是核心挑战。
  • 使用 headless‑Chrome 爬虫 + 智能后处理可以导出一个在任何地方都能正常运行的完整静态副本。

概览

没有 CDN 依赖,没有 API 调用,也没有框架。打开浏览器中的 index.html 即可运行。

文件大小大幅下降。典型的 Framer 网站会加载 800 KB+ 的 JavaScript(React 运行时、Framer 库、hydration 包)。导出后的版本通常总计 不足 100 KB 的 JS(仅包含小的交互脚本)。

定价及一次性原因

$10.99 每个 Framer URL。 您只需一次性付款,下载 ZIP,即可完成。

我选择一次性模式是因为使用场景是交易性的:您导出站点一次,可能几个月后在 Framer 中进行更改后再次导出。这不是每天或每周都会进行的操作。

  • 参考:Framer 的 Pro 计划是 $30/月 每站点
  • 如果您导出后迁移到 Vercel 的免费层,之后每月可节省 $30
  • $10.99 大约 11 天即可收回成本。

我想对其他开发导出工具的开发者说的话

  • 不要相信初始 HTML。
    任何在客户端进行 hydration 的站点(React、Vue、Svelte 等)只会给你一个快照,无法代表真实页面。请在真实浏览器中渲染并等待 JavaScript 完成。

  • 滚动页面。
    懒加载现在无处不在。如果不滚动,就会错过一半的内容。

  • 留意由 JS 驱动的样式。
    越来越多的网站通过 JavaScript 而非 CSS 来实现视觉状态——悬停效果、滚动触发、Intersection Observer 等。要捕获视觉行为,需要模拟用户交互并观察 DOM 变化。

  • 在发布前在 20+ 个真实站点上测试。
    每个 Framer 站点使用的组件组合略有不同。边缘情况层出不穷:轮播图、标签页、嵌套手风琴、粘性标题、视频背景等。每一种都需要特定的处理。

亲自尝试

如果您想尝试,请访问 letaiworkforme.com。导出和实时预览是免费的;只有下载 ZIP 时才需要付费。

代码运行在 Node.js + Puppeteer 上。欢迎随时提问技术细节!

0 浏览
Back to Blog

相关文章

阅读更多 »