运行环境:background(后台应用)
// @match background 是 @match 的保留字:声明后应用不注入任何网页,而是常驻运行在扩展的共享后台沙箱里——适合轮询、定时任务、给终端提供 CLI 命令等不依赖页面的能力。
| 声明方式 | executionTarget | 行为 |
|---|---|---|
只有 URL 型 @match(或不写) | page | 现状:注入命中的网页 |
只有 // @match background | background | 只在后台沙箱运行一个常驻实例,不注入任何网页 |
| 两者都有 | both | 双实例并行:后台实例常驻 + 网页实例按匹配规则注入,互不影响 |
background ≠ 扩展的 background service worker
后台应用运行在 offscreen 文档内的 sandbox iframe(opaque origin)里,没有任何 chrome.* 扩展 API(不能调 chrome.tabs、chrome.storage 等)。它的能力集与页面应用的 LFRuntime 内置库一致(减去页面 DOM/location 语义),不因换环境获得额外权限。
环境差异
- 无页面 DOM:
document存在但不可见,querySelector常返回 null;不要把 DOM 挂载当作交付 UI,LFDOM 的页面能力不可用 - opaque origin:直接访问
localStorage/IndexedDB会抛 SecurityError;持久化请用LFStore(走扩展存储,跨环境共享同一份数据) - 生命周期:随浏览器启动常驻,不依赖任何标签页;扩展重启后自动恢复运行
@include/@exclude/@exclude-match/@intercept/@run-at只约束 page 分支,对后台分支无效
内置库可用性
后台沙箱没有自己的 chrome.*,但它拿到一套最小 Port shim:所有出站消息经 offscreen broker 代发、由 background 逐条鉴权,因此内置库的既有通道代码原样工作。换句话说,除了真正需要页面/标签页的能力,其余内置库在后台分支都可用。
| 能力 | 后台分支 | 说明 |
|---|---|---|
LFStore(含 watch) | 可用 | 与页面分支共享同一命名空间,是 both 应用两个分支之间同步状态的推荐通道 |
LFModel(含 chatStream) | 可用 | 流式走 Port,与页面分支一致 |
LFUser | 可用 | 登录态与用户信息读取正常 |
LFHttp | 可用 | request / stream / websocket / client.* / server.* 均正常;page.* 需要标签页,返回 TAB_UNAVAILABLE |
LFRuntime.cli | 可用 | 注册的命令出现在 plugincmd list --source background,不依赖任何标签页 |
LFRuntime.sidebar | 部分可用 | getState / getAgentState / input / setModel / close 正常;open() 需要用户手势和调用方窗口,后台没有,会被拒绝 |
LFRuntime 基础能力 | 可用 | sleep / registerCleanup / print 等 |
工作区桥(readFile / writeFile / execTerminalCommand) | 仅经租约 | 与页面应用相同:应用源码直接调用会编译报错,须在 LFRuntime.cli.register({ workspace: [...] }) 的 handler 里用 ctx.workspace.*,租约只在本次 plugincmd call 期间有效。run --target background 临时脚本可直接调用 |
LFDOM | 不可用 | 库能加载,但没有页面 DOM 可读:查询返回空结果而不是抛错;click / fill / waitFor 等依赖可见性的操作会超时(提示 no web page in the background environment),waitForResponse / waitForRequest 以 LF_NETWORK_NO_TAB 拒绝 |
LFNetwork | 不可用 | 要把 CDP 附到标签页上,start() 直接抛 LF_NETWORK_NO_TAB |
LFRuntime.input | 不可用 | 只驱动脚本自己所在的标签页,后台没有,直接抛 RUNTIME_INPUT_NO_TAB |
LFRuntime.runInMainWorld | 不可用 | 没有页面可注入,调用会 reject MAIN_WORLD_NO_TAB |
websocket 用默认 mode
后台沙箱是 opaque origin,CSP 会拦掉原生 WebSocket,mode: "direct" 连不上。保持默认的 mode: "auto" 即可——它会自动选 background 模式,把 socket 开在扩展后台再把事件回投过来。
设计上的推论:后台分支适合承载数据、轮询、CLI 与对外接口,凡是要碰页面的都放到 page 分支去;both 应用两个分支之间用 LFStore(配合 LFStore.watch)而不是共享内存来传递状态。
判断当前执行环境
both 应用的后台分支与页面分支执行同一份代码,用 LFRuntime.executionEnvironment 在入口分流即可——它在生成产物时就烘焙成常量,不做运行时探测,因此可以安全地写在最顶层。不要靠嗅探 chrome.* 是否缺失、document.querySelector 是否恒返回 null、或捕获 localStorage 的 SecurityError 来推断环境(这些都是内部实现细节)。
| 取值 | 含义 |
|---|---|
page | 注入到网页:已启用/已上架的 page 分支、liveDebug、run --tab。有页面 DOM 与 location |
background | 共享 offscreen 沙箱:后台应用分支、run --target background。可用 LFRuntime.isBackground 快捷判断 |
editor-sandbox | 不带 --tab 的 run:跑在编辑器页的隐藏 iframe,没有任何目标网页 |
两个分支之间共享状态请走 LFStore + LFStore.watch(后台写、页面订阅渲染)。注意 LFRuntime.liveDebug 是「开发期 / 发布态」的区分,与执行环境无关,两者不要混用。
示例:both 应用的双分支
// ==UserScript==
// @name Rate Panel
// @description 后台轮询 + 页面展示,同一份代码两个分支
// @match background
// @match https://example.com/*
// @require store
// @require http
// ==/UserScript==
// executionEnvironment 在生成产物时就已烘焙成常量,可直接在入口分流
if (LFRuntime.isBackground) {
// 后台分支:只做取数与写入,没有 DOM 交付面
const pull = async () => {
const data = await LFHttp.json("https://open.er-api.com/v6/latest/USD");
await LFStore.set("usd-cny", { rate: data.rates.CNY, at: Date.now() });
};
pull();
LFRuntime.interval(pull, 10 * 60 * 1000);
} else if (LFRuntime.executionEnvironment === "page") {
// 页面分支:只渲染,数据经 LFStore.watch 与后台分支保持同步
LFRuntime.mountHot("rate-panel", (hot) => {
const el = LFRuntime.trackNode(document.createElement("div"));
document.body.appendChild(el);
const render = (value) => {
el.textContent = value ? `1 USD = ${value.rate} CNY` : "等待后台取数…";
};
const unwatch = LFStore.watch("usd-cny", ({ value }) => render(value), { immediate: true });
hot.onDispose(unwatch);
});
}开发与测试
run --target background <入口>在与正式后台应用完全相同的沙箱环境里执行测试脚本(与--tab/--focus/--record/--world user-script互斥)liveDebug on对 background / both 应用同样可用:后台分支热更新按「整实例替换」执行;both 应用没有匹配网页时后台分支照常运行,页面分支进入 waiting-for-targetplugincmd默认优先调用 background 实例注册的命令,可用--source background|page显式指定app list的 TARGET 列与应用管理页的「后台应用 / 后台+网页」标签展示运行环境。网页对话模式的「本页应用」弹层中,后台应用与「页面分支未命中当前页」的 both 应用恒显示在「后台运行」分组(both 应用带徽标:命中本页时标「后台+本页」,未命中时标「后台+网页」),随时可在此启停
示例:常驻价格监控
main.js
// ==UserScript==
// @name Price Watcher
// @description 常驻后台轮询价格,无需打开任何页面
// @match background
// @require store
// @require http
// ==/UserScript==
async function checkOnce() {
const data = await LFHttp.json("https://open.er-api.com/v6/latest/USD");
await LFStore.set("usd-cny", { rate: data.rates.CNY, at: Date.now() });
}
checkOnce();
LFRuntime.interval(checkOnce, 10 * 60 * 1000);
LFRuntime.cli.register({
name: "rate",
brief: "查看最新缓存汇率",
handler: async () => {
const cached = await LFStore.get("usd-cny", null);
return cached ? `1 USD = ${cached.rate} CNY` : "尚未获取到汇率";
},
});