一个旨在帮助开发者快速构建功能强大、可维护的现代油猴脚本的框架。它提供模块化架构、自动化的UI设置面板、响应式生命周期管理、高性能DOM调度器与网络请求拦截器等核心功能,让您专注于实现创意,而非繁琐的底层细节。
ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @require https://update.greasyfork.org/scripts/545792/1911233/Caliberjs%20Framework%20Library.js.
Caliber.js 是用于油猴(Userscript)脚本的模块化开发框架,提供配置管理、设置面板渲染、DOM 分帧调度、网络请求拦截、宿主页面代码执行与模块生命周期管理等核心功能。
在脚本元数据块中引入 Caliber.js,通过继承 Caliber.Module 编写功能模块,并调用 Caliber.createApp 启动:
// ==UserScript==
// @name Demo Script
// @namespace https://example.com/
// @version 1.0.0
// @match https://example.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @require https://update.greasyfork.org/scripts/545792/Caliberjs%20Framework%20Library.js
// ==/UserScript==
class DemoModule extends Caliber.Module {
id = "demo-module";
name = "示例模块";
description = "用于演示基本功能的示例模块。";
defaultConfig = {
enabled: true,
textColor: {
type: "color",
value: "#1a73e8",
label: "文字颜色"
}
};
async onEnable(context) {
this._logger.log("模块已启用,URL 参数:", context.query);
}
async onDisable() {
this._logger.log("模块已停用。");
}
}
(async () => {
await Caliber.createApp({
appName: "示例脚本",
modules: [DemoModule],
services: {
storage: {
get: async () => JSON.parse((await GM_getValue("app_config", "{}"))),
set: async (val) => GM_setValue("app_config", JSON.stringify(val))
},
command: {
register: (name, cb) => GM_registerMenuCommand(name, cb)
}
}
});
})();
createApp)Caliber.createApp(options) 是应用的入口方法,负责依赖装配与生命周期初始化。
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
appName |
string |
是 | - | 应用名称,用于日志前缀、设置面板标题及菜单项命名 |
modules |
Array<typeof Module> |
是 | - | 注册的模块类数组(传入类本身) |
services |
object |
是 | - | 平台环境适配器集合 |
services.storage |
StorageAdapter |
否 | localStorage | 包含 get(): Promise<object> 和 set(val): Promise<void> |
services.command |
CommandAdapter |
否 | 空实现 | 包含 register(name, callback): void |
services.hostWindow |
Window |
否 | 自动识别 | 宿主窗口对象,默认优先使用 unsafeWindow,回退至 window |
services.hostDocument |
Document |
否 | document |
宿主文档对象 |
services.style |
StyleAdapter |
否 | 自动生成 | 样式注入适配器,提供 add(css, id) 和 remove(id) |
isDebug |
boolean |
否 | false |
是否开启调试模式与资源泄漏审计 |
settingsPanelEnabled |
boolean |
否 | true |
设置面板功能初始启用状态 |
settingsPanel |
SettingsPanelOptions |
否 | {} |
设置面板与触发器的配置对象 |
framework.domProcessorBatchSize |
number |
否 | 20 |
DOM 调度器单帧处理的最大节点数 |
settingsPanel)settingsPanel 支持配置面板的定位、拖拽、吸附、展开形态及自定义触发器。
interface SettingsPanelOptions {
enabled?: boolean; // 是否启用设置面板 (默认: true)
draggable?: boolean; // 是否允许拖拽悬浮按钮 (默认: true)
edgeSnapping?: boolean; // 拖拽释放时是否吸附至屏幕边缘 (默认: true)
snapThreshold?: number; // 触发边缘吸附的距离阈值 (默认: 25px)
right?: number; // 距屏幕右侧距离 (默认: 0)
bottom?: number; // 距屏幕底部距离 (默认: 50)
left?: number; // 距屏幕左侧距离 (设置时优先于 right)
top?: number; // 距屏幕顶部距离 (设置时优先于 bottom)
modalWidth?: string; // 居中弹窗形态下的最大宽度 (默认: "720px")
trigger?: string | HTMLElement; // 外部 DOM 绑定触发器(选择器或节点对象)
floatingContent?: string | HTMLElement; // 自定义悬浮按钮内容(DOM 节点或 HTML)
}
left、top、right、bottom。若同时传入 left 与 right,优先使用 left;若同时传入 top 与 bottom,优先使用 top。document.documentElement.clientWidth 计算视口可用宽度,并结合窗口 resize 事件限制坐标范围,避免元素溢出视口边界。draggable: true 时,悬浮按钮支持指针事件拖拽。edgeSnapping: true 时,释放悬浮按钮后,若与屏幕左、右、上、下边缘的距离小于 snapThreshold,将自动贴靠至对应边缘并持久化坐标数据。打开设置面板时,根据触发按钮的当前坐标自动选择展示形态:
360px,高 100%,向左滑出);360px,高 100%,向右滑出);modalWidth 控制,高度固定为 min(78vh, 680px),包含关闭按钮)。trigger):传入外部选择器字符串或 DOM 元素后,默认悬浮按钮将被隐藏,点击指定的外部 DOM 即可打开设置面板:
javascript
settingsPanel: {
trigger: "#navbar-settings-btn"
}
floatingContent):传入 HTMLElement 或 HTML 字符串后,默认的悬浮按钮将移除自带背景、边框和尺寸限制,转为包裹传入内容的容器,并保留拖拽与坐标逻辑:
```javascript
const customNode = document.createElement("div");
customNode.textContent = "设置";settingsPanel: { floatingContent: customNode }
#### 5. 全局事件控制
可通过全局事件总线控制面板开闭:
```javascript
this._eventBus.emit("command:open-settings-panel"); // 打开面板
this._eventBus.emit("command:close-settings-panel"); // 关闭面板
this._eventBus.emit("command:toggle-settings-panel"); // 切换状态
Module)所有功能模块须继承 Caliber.Module 基类。
class MyModule extends Caliber.Module {
id = "my-module"; // 唯一标识符(不可使用 "base-module")
name = "模块名称"; // 面板中显示的名称
description = "模块功能描述"; // 面板中显示的描述
match = "/user/:id"; // 页面匹配规则(可选)
uiGuard = null; // UI 守护配置(可选)
}
defaultConfig)defaultConfig 用于定义模块可配置项及生成对应的设置面板表单控件。
defaultConfig = {
enabled: true, // 必填/保留字段,控制模块启用状态
// 字符串输入框
textKey: {
type: "string",
value: "默认值",
label: "文本配置项",
description: "配置说明",
inputProps: { placeholder: "请输入..." }
},
// 多行文本域
textareaKey: {
type: "textarea",
value: "行1\n行2",
label: "多行文本项",
inputProps: { rows: 3 }
},
// 数值输入框
numberKey: {
type: "number",
value: 10,
label: "数值配置项",
inputProps: { min: 0, max: 100, step: 1 }
},
// 布尔开关
switchKey: {
type: "boolean",
value: false,
label: "开关配置项"
},
// 下拉选择框
selectKey: {
type: "select",
value: "opt1",
label: "选择配置项",
options: [
{ label: "选项一", value: "opt1" },
{ label: "选项二", value: "opt2" }
]
},
// 颜色选择器
colorKey: {
type: "color",
value: "#000000",
label: "颜色配置项"
}
};
indentLevel (1 | 2 | 3): 配置项左侧层级缩进。divider ("top" | "bottom" | "both"): 添加分割线。inputProps (object): 透传给原生 HTML 标签的属性对象(如 min、max、rows、placeholder 等)。所有异步生命周期方法均按排他队列串行执行。
| 方法名 | 异步支持 | 触发条件 | 参数 |
|---|---|---|---|
onEnable(context) |
是 | 模块启用且当前 URL 匹配成功时执行 | context: { params: object, query: object } |
onDisable() |
是 | 模块停用或页面离开匹配范围时执行。 需在此方法中注销事件监听与定时器 |
无 |
onConfigChange(key, newVal, oldVal) |
否 | 模块专属配置项变更时触发 | key: 配置键名newVal: 新值oldVal: 旧值 |
onNavigate(context) |
是 | 模块处于激活状态且当前 SPA 页面发生路由切换时触发 | context: { params: object, query: object } |
onRender(targetElement) |
否 | uiGuard 目标节点存在且组件丢失时触发 |
targetElement: 挂载父节点 |
onCleanup() |
否 | uiGuard 组件被卸载时触发 |
无 |
模块实例内部可通过 this.xxx 访问框架服务:
this._logger // 日志输出实例
this._eventBus // 全局事件总线 (on, off, emit)
this._hostWindow // 宿主 Window 对象
this._hostDocument // 宿主 Document 对象
this._scheduler // DOM 批量调度器
this._interceptor // 网络请求拦截器
this._sanitizer // DOM 净化与安全注入服务
this._executor // 页面作用域执行器
this._utils // 实用工具集合 (包含 checkMatch 路由检查)
this._module // 模块操作门面 (包含 requestReset 重启方法)
_scheduler)基于统一 MutationObserver 与 requestAnimationFrame 实现的批量 DOM 监听器。
// 注册任务
const taskId = this._scheduler.register(
".target-selector",
(element) => {
// 匹配元素处理逻辑
},
{
add: true, // 监听节点插入 (默认: true)
attributes: false, // 监听属性变更 (默认: false)
attributeFilter: ["class"], // 指定监听的属性列表 (可选)
root: "#container", // 限制监听范围的根选择器或节点 (可选)
processExisting: true // 注册时立即处理页面已存在的匹配节点 (默认: false)
}
);
// 注销任务
this._scheduler.unregister(taskId);
_interceptor)用于拦截和修改页面的 Fetch 与 XMLHttpRequest 网络请求。
this._interceptor
.target({
url: "https://api.example.com/data", // 目标 URL
method: "GET", // 请求方法 (大写)
match: "/path/*" // 生效路由 (可选)
})
.onRequest(`
// 运行于页面上下文的纯函数体字符串
urlObject.searchParams.set('from', 'caliber');
return { url: urlObject.toString(), config };
`)
.onResponse((responseData) => {
// 运行于沙箱上下文的响应数据回调
console.log("响应数据:", responseData);
})
.register(this.id);
// 移除指定 ID 的拦截器
this._interceptor.removeHook(this.id);
_executor)在宿主页面的真实作用域中执行代码并异步返回可序列化的结果,内置 10 秒超时机制。
try {
const result = await this._executor.execute(`
(() => {
return window.__PAGE_DATA__ || null;
})()
`);
} catch (error) {
this._logger.error("代码执行失败:", error.message);
}
_sanitizer)用于处理 CSP (Content Security Policy) 和 Trusted Types 环境下的安全 DOM 操作。
// 1. 设置 innerHTML
this._sanitizer.setInnerHTML(element, "<span>内容</span>");
// 2. 创建 TrustedHTML
const html = this._sanitizer.createTrustedHTML("<div>内容</div>");
// 3. 注入脚本
this._sanitizer.injectScript(this._hostDocument, "console.log('injected')");
// 4. 注入样式
this._sanitizer.injectStyle(this._hostDocument, ".rule { color: red; }", "style-id");
_module)用于请求对当前模块执行完整的停用与重新启用流程。
await this._module.requestReset();
uiGuard)用于在 SPA 单页应用中维持自定义 UI 组件的挂载状态。
class CustomUIModule extends Caliber.Module {
id = "custom-ui";
name = "界面注入模块";
uiGuard = {
target: "#header", // 挂载目标的容器选择器
component: ".injected-ui" // 组件自身选择器,用于检测是否存在
};
onRender(targetElement) {
const node = this._hostDocument.createElement("div");
node.className = "injected-ui";
node.textContent = "挂载组件";
targetElement.appendChild(node);
}
onCleanup() {
this._hostDocument.querySelector(this.uiGuard.component)?.remove();
}
}
match)match 属性支持匹配 URL 路径并提取动态参数:
// 1. 动态 RESTful 命名参数 -> context.params.userId
match = "/user/:userId";
// 2. 可选参数
match = "/page/:id?";
// 3. 通配符 -> context.params._
match = "/article/*";
// 4. 正则表达式
match = /^\/post\/\d+$/;
// 5. 规则数组
match = ["/home", "/dashboard", "/user/*"];
ModuleAuditor)当 createApp 配置 isDebug: true 时启用,在模块执行 onDisable() 完成后检测未清理的资源:
setInterval 定时器;_scheduler 任务。检测到未清理资源时,将在控制台输出错误日志。
destroy)用于注销应用实例并清理框架占用的全局资源:
const kernelKey = Object.keys(window).find(k => k.startsWith("CALIBER_INSTANCE_"));
const kernel = window[kernelKey];
if (kernel) {
await kernel.destroy();
}
执行后将依次触发已激活模块的 onDisable()、移除设置面板、清理网络拦截 Hook 并释放全局引用。
// ==UserScript==
// @name VideoHelper
// @namespace https://example.com/
// @version 1.0.0
// @match https://example.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @require https://update.greasyfork.org/scripts/545792/Caliberjs%20Framework%20Library.js
// ==/UserScript==
class VideoDownloadModule extends Caliber.Module {
id = "video-downloader";
name = "视频下载工具";
description = "获取视频源地址并添加下载按钮。";
match = "/video/:id";
defaultConfig = {
enabled: true,
themeColor: {
type: "color",
value: "#1a73e8",
label: "按钮颜色"
},
filterRules: {
type: "textarea",
value: "rule1\nrule2",
label: "过滤规则",
inputProps: { rows: 2 }
}
};
uiGuard = {
target: ".video-actions",
component: ".btn-download"
};
#sourceUrl = null;
#taskId = null;
async onEnable(context) {
this._logger.log("当前视频 ID:", context.params.id);
this._interceptor
.target({
url: window.location.origin + "/api/video/source",
method: "GET"
})
.onResponse((data) => {
if (data?.url) {
this.#sourceUrl = data.url;
}
})
.register(this.id);
this.#taskId = this._scheduler.register(
".video-title",
(el) => {
el.style.color = this._config.themeColor;
},
{ processExisting: true }
);
}
onRender(targetElement) {
const btn = this._hostDocument.createElement("button");
btn.className = "btn-download";
btn.textContent = "下载视频";
btn.style.cssText = `padding: 4px 8px; color: #fff; background: ${this._config.themeColor}; border: none; border-radius: 4px; cursor: pointer;`;
btn.onclick = () => {
if (this.#sourceUrl) {
window.open(this.#sourceUrl, "_blank");
}
};
targetElement.appendChild(btn);
}
onCleanup() {
this._hostDocument.querySelector(this.uiGuard.component)?.remove();
}
onConfigChange(key, newValue) {
if (key === "themeColor") {
const btn = this._hostDocument.querySelector(this.uiGuard.component);
if (btn) btn.style.backgroundColor = newValue;
}
}
async onDisable() {
this._interceptor.removeHook(this.id);
if (this.#taskId) {
this._scheduler.unregister(this.#taskId);
this.#taskId = null;
}
this.onCleanup();
this.#sourceUrl = null;
}
}
(async () => {
await Caliber.createApp({
appName: "VideoHelperApp",
isDebug: true,
modules: [VideoDownloadModule],
settingsPanel: {
draggable: true,
edgeSnapping: true,
bottom: 50,
right: 0
}
});
})();