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.rs | pake.json 的反序列化结构体 |
app/invoke.rs | Tauri 命令(下载、通知、Dock Badge 等) |
app/menu.rs | macOS 原生菜单栏 |
inject/*.js | 注入到网页的 JavaScript |
窗口创建流程
set_window() 在 app/window.rs 中,是一个精心编排的构建器链:
- WebView 初始化:根据
url_type决定加载远程 URL 还是本地文件 - User-Agent 伪装:按平台设置不同的 UA,避免网站拒绝非浏览器请求
- 初始化脚本注入:按顺序注入 8 个 JS 文件(顺序很重要)
- 平台适配:
- macOS:TitleBarStyle(Overlay/Visible)、Theme
- Windows:高 DPI 缩放补偿、
--disable-features=msWebOOUI禁用 Edge 干扰 - Linux:
WEBKIT_DISABLE_DMABUF_RENDERER环境变量修复渲染问题
- 下载拦截:通过
on_download回调把浏览器下载重定向到系统下载目录 - 新窗口处理:
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_fileTauri 命令,写入系统下载目录 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-oauth | OAuth 认证流程支持 |
tauri-plugin-http | HTTP 请求代理 |
tauri-plugin-shell | 打开外部链接 / 系统浏览器 |
tauri-plugin-notification | 原生系统通知 |
tauri-plugin-opener | 文件和 URL 打开 |
体积为什么这么小?
对比 Electron 应用(通常 100-200MB),Pake 只有约 5MB。核心原因:
- 系统 WebView:Tauri 使用操作系统的内置 WebView(macOS 的 WKWebView、Windows 的 WebView2、Linux 的 WebKitGTK),不需要打包整个 Chromium
- Rust 编译优化:
Cargo.toml中配置了opt-level = "z"(最小体积优化)、lto = "thin"(链接时优化)、strip = true(去除符号) - 按需编译:只编译使用到的 Tauri features
- 无 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
评论互动