Pake 技术原理深度分析:用 Rust 把任意网页打包成桌面应用

发布于 2026年06月24日 17:19 #DevOps#CLI

Pake 技术原理深度分析:用 Rust 把任意网页打包成桌面应用 封面图
  • 整体架构分为 TypeScript CLI 层和 Rust Tauri 应用层,CLI 动态生成配置并嵌入二进制
  • 通过注入 JavaScript 和 CSS 实现窗口拖拽、快捷键、链接拦截、原生通知等桌面交互
  • 利用系统 WebView 和 Rust 编译优化实现约 5MB 的小体积
  • 支持多平台构建和丰富的 Tauri 插件(窗口状态、单实例、全局快捷键等)
  • 配置驱动模式:同一二进制模板配合不同 pake.json 即可生成不同桌面应用

Pake 是一个让人眼前一亮的开源项目:一行命令,把任意网页打包成 macOS / Windows / Linux 桌面应用,体积只有 Electron 方案的 二十分之一(通常 5M 左右)。项目在 GitHub 上已获得数万 Star,社区贡献了包括 ChatGPT、Twitter、DeepSeek、YouTube Music 在内的几十个热门应用的打包版本。

但更让我感兴趣的是它的技术实现——一个 TypeScript CLI 工具 + Rust 运行时,如何在 不写一行网页前端代码 的前提下,把一个任意网站变成体验良好的桌面应用?

这篇文章从源码层面拆解 Pake 的核心架构。

整体架构

Pake 分为两层:

┌─────────────────────────────────┐
│  CLI 层 (TypeScript / Rollup)   │
│  - 命令行参数解析                 │
│  - 图标处理 (sharp / icon-gen)   │
│  - 配置生成 (merge to pake.json) │
│  - 跨平台构建调度                 │
└──────────────┬──────────────────┘
               │ 生成 pake.json + tauri.conf.json

┌─────────────────────────────────┐
│  Tauri 应用层 (Rust)            │
│  - WebView 窗口管理              │
│  - JS/CSS 注入引擎               │
│  - 系统托盘 & 全局快捷键          │
│  - 下载管理 & 通知               │
│  - 多窗口支持                    │
└──────────────┬──────────────────┘
               │ 加载

┌─────────────────────────────────┐
│  目标网站 (任意 URL)             │
│  - 被注入 custom CSS/JS         │
│  - 获得原生桌面能力              │
└─────────────────────────────────┘

关键设计理念:Pake 本身不需要知道目标网站的任何细节。它通过一个通用且可配置的注入层,把原生桌面能力“嫁接”到任意网页上。

CLI 层:从命令到配置

当用户执行 pake https://example.com --name MyApp 时,CLI 的职责是:

1. 解析选项 → 生成 pake.json

CLI 入口在 bin/cli.ts,通过 commander 解析参数,交给 handleInputOptions() 处理后创建对应平台的 Builder

核心配置生成在 helpers/merge.ts 中,将用户选项(窗口大小、标题栏隐藏、UA、代理等)合并写入 src-tauri/.pake/pake.json

{
  "windows": [{
    "url": "https://example.com",
    "width": 1200,
    "height": 780,
    "hide_title_bar": true,
    "user_agent": "...",
    ...
  }],
  "system_tray": { "macos": false, "linux": true, "windows": true },
  "multi_instance": false,
  "multi_window": false
}

2. 图标处理

options/icon.ts 负责图标的完整流水线:

  • 从 URL 下载或用默认图标
  • sharp 调整尺寸
  • icon-gen 生成各平台格式(.icns / .ico / .png

3. 触发 Tauri 构建

BaseBuilder.buildAndCopy() 通过 shellExec 调用 pnpm run build --config .pake/tauri.conf.json --features cli-build

cli-build feature 是关键——它让 Rust 代码在编译时通过 include_str! 把 CLI 生成的 .pake/pake.json 嵌入到二进制中,而非使用仓库默认的 pake.json

// src-tauri/src/util.rs
#[cfg(feature = "cli-build")]
let pake_config: PakeConfig =
    serde_json::from_str(include_str!("../.pake/pake.json"))
        .expect("Failed to parse pake config");

这就是“一行命令打包任意网站”的魔法核心:CLI 动态生成配置 → 嵌入 Rust 二进制 → Tauri 编译出最终应用。

Rust 运行时:WebView 窗口 + JS 注入

Pake 的 Rust 层基于 Tauri v2,核心逻辑在 src-tauri/src/ 下:

文件职责
lib.rs应用入口,组装所有插件和事件处理
app/window.rs窗口创建、WebView 配置、下载拦截
app/setup.rs系统托盘、全局快捷键
app/config.rspake.json 的反序列化结构体
app/invoke.rsTauri 命令(下载、通知、Dock Badge 等)
app/menu.rsmacOS 原生菜单栏
inject/*.js注入到网页的 JavaScript

窗口创建流程

set_window()app/window.rs 中,是一个精心编排的构建器链:

  1. WebView 初始化:根据 url_type 决定加载远程 URL 还是本地文件
  2. User-Agent 伪装:按平台设置不同的 UA,避免网站拒绝非浏览器请求
  3. 初始化脚本注入:按顺序注入 8 个 JS 文件(顺序很重要)
  4. 平台适配
    • macOS:TitleBarStyle(Overlay/Visible)、Theme
    • Windows:高 DPI 缩放补偿、--disable-features=msWebOOUI 禁用 Edge 干扰
    • Linux:WEBKIT_DISABLE_DMABUF_RENDERER 环境变量修复渲染问题
  5. 下载拦截:通过 on_download 回调把浏览器下载重定向到系统下载目录
  6. 新窗口处理on_new_window 拦截 window.open() 调用

CSS 注入:标题栏拖拽区

inject/style.js 是整个注入系统中最精妙的设计之一。当 hide_title_bar: true 时,它会注入一个透明的拖拽手柄:

#pake-top-dom {
  position: fixed;
  background: transparent;
  top: 0;
  width: 100%;
  height: 20px;
  cursor: grab;
  -webkit-app-region: drag;  /* 关键:让这个区域可以拖拽窗口 */
  z-index: 99999;
}

-webkit-app-region: drag 是 macOS WebView 的私有 CSS 属性,让网页中的某个区域变成窗口拖拽手柄。Pake 利用这个特性,在隐藏原生标题栏的同时保留窗口拖拽能力,体验非常原生。

同时,style.js 还注入了大量站点特定的 CSS 规则来优化体验:

  • 移除广告横幅(.adsbygoogle 等)
  • 调整顶部间距(因为隐藏了标题栏,内容需要往下偏移 20-30px)
  • 隐藏不需要的侧边栏(Twitter/X、ChatGPT 等)
  • 优化响应式布局(让内容居中、限制最大宽度)

JavaScript 注入:事件与交互

inject/event.js 是注入层的核心,约 800 行代码,实现了完整的桌面级交互:

键盘快捷键映射

const shortcuts = {
  "[":  () => window.history.back(),     // Cmd+[ 后退
  "]":  () => window.history.forward(),  // Cmd+] 前进
  "r":  () => window.location.reload(),  // Cmd+R 刷新
  "-":  () => zoomOut(),                // Cmd+- 缩小
  "=":  () => zoomIn(),                 // Cmd+= 放大
  ...
};

链接拦截系统:这是最复杂的部分。Pake 需要判断每个链接点击的行为:

  • 同域名链接:在 WebView 内正常跳转
  • 外部域名链接:调用 plugin:shell|open 用系统浏览器打开
  • 下载链接:触发 download_file Tauri 命令,写入系统下载目录
  • blob: / data: 协议:走原生下载(因为严格 CSP 的网站会阻止 IPC 通信)
  • OAuth 弹窗:识别认证流程链接,保持在应用内打开
  • _blank 目标链接:根据 force_internal_navigation 或域名规则决定行为

右键菜单:Pake 自己实现了一套右键菜单,支持:

  • 下载图片 / 视频
  • 复制链接地址
  • 在浏览器中打开
  • 自动适配深色 / 浅色主题

缩放系统

function setZoom(zoom) {
  if (isWindows) {
    body.style.transform = `scale(${zoomValue})`;
    body.style.transformOrigin = "top left";
    body.style.width = `${100 / zoomValue}%`;
  } else {
    html.style.zoom = zoom;  // macOS 原生支持
  }
}

Windows 和 macOS 用了完全不同的缩放策略——macOS WebView 原生支持 zoom CSS 属性,而 Windows 需要用 transform: scale() 模拟,同时调整容器尺寸。

Web Notification → 原生通知桥接:Pake 用 IIFE(立即执行函数)在页面脚本运行之前替换了 window.Notification,让网页内的通知调用自动转为原生系统通知,同时驱动 macOS Dock 角标。

// 替换 window.Notification
Object.defineProperty(window, "Notification", {
  value: wrappedNotification,  // 内部调用 invoke("send_notification", ...)
});

其他注入脚本

文件功能
auth.js检测 OAuth 弹窗链接,让认证流程保持在应用内
custom.js用户自定义注入脚本的占位文件
find.js页面内查找功能(Cmd+F)
fullscreen.js全屏 API polyfill,支持 --fullscreen 标志和 F11 快捷键
toast.js轻量级 Toast 通知组件(用于下载状态提示)
theme_refresh.js监听系统主题变化并刷新页面

多平台构建

Pake 通过 builders/ 目录下的 Builder 模式处理平台差异:

bin/builders/
├── BaseBuilder.ts     # 抽象基类(通用构建逻辑)
├── BuilderProvider.ts # 工厂方法(按平台创建对应 Builder)
├── MacBuilder.ts      # macOS (.app / .dmg)
├── WinBuilder.ts      # Windows (.msi / .nsis)
├── LinuxBuilder.ts    # Linux (.deb / .AppImage)
└── env.ts             # 构建环境检测与配置

每个平台 Builder 继承 BaseBuilder,覆盖 getFileName()getBuildCommand() 等方法。比如 MacBuilder 默认生成 .app bundle,而 LinuxBuilder 支持 .deb.AppImage 两种格式,并内置了 AppImage 构建失败时的自动重试(NO_STRIP=1 处理 glibc 2.38+ 兼容性)。

构建时还支持:

  • 多架构:通过 --multi-arch 生成 universal binary
  • 中国镜像:通过 CN_MIRROR=1 环境变量使用国内 Rust 镜像加速
  • 调试模式--debug 启用详细日志和 dev 构建

插件体系

Pake 利用 Tauri v2 的插件系统获得丰富能力:

插件用途
tauri-plugin-window-state记住窗口位置和大小
tauri-plugin-single-instance单实例运行(第二次启动时激活已有窗口)
tauri-plugin-global-shortcut全局快捷键(如 Alt+Space 呼出窗口)
tauri-plugin-oauthOAuth 认证流程支持
tauri-plugin-httpHTTP 请求代理
tauri-plugin-shell打开外部链接 / 系统浏览器
tauri-plugin-notification原生系统通知
tauri-plugin-opener文件和 URL 打开

体积为什么这么小?

对比 Electron 应用(通常 100-200MB),Pake 只有约 5MB。核心原因:

  1. 系统 WebView:Tauri 使用操作系统的内置 WebView(macOS 的 WKWebView、Windows 的 WebView2、Linux 的 WebKitGTK),不需要打包整个 Chromium
  2. Rust 编译优化Cargo.toml 中配置了 opt-level = "z"(最小体积优化)、lto = "thin"(链接时优化)、strip = true(去除符号)
  3. 按需编译:只编译使用到的 Tauri features
  4. 无 JS 运行时:不打包 Node.js,CLI 只是构建工具,不进入最终产物
opt-level = "z"     # 最小体积(而非速度)
lto = "thin"        # 跨 crate 死代码消除
codegen-units = 1   # 单个编译单元,最大化优化
strip = true        # 去除调试符号
panic = "abort"     # 去除 unwind 支持代码

总结

Pake 的技术架构体现了“少即是多”的工程哲学:

  • CLI 层用 TypeScript 做灵活的配置生成,图标处理和构建编排
  • Rust 层用 Tauri 做精简的 WebView 容器,不打包 Chromium 和 Node.js
  • 注入层用纯 JavaScript/CSS 做站点适配,通过 include_str! 在编译时嵌入
  • 配置驱动:同一个二进制模板 + 不同的 pake.json = 不同的桌面应用

这种“模板化应用 + 运行时配置”的架构模式,很适合把 Web 服务快速桌面化的场景。如果你有内部工具、SaaS 后台、或者常用的 Web 应用想要一个独立的桌面客户端,Pake 的技术路线值得参考。


Pake 项目地址:github.com/tw93/Pake

评论互动

© 2026 王若风的技术博客 · Powered by Astro