首页
/ uBlock Origin npm 包 @gorhill/ubo-core 实战指南:在 Node.js 中运行静态网络过滤引擎 SNFE 与 HNTrie

uBlock Origin npm 包 @gorhill/ubo-core 实战指南:在 Node.js 中运行静态网络过滤引擎 SNFE 与 HNTrie

2026-09-03 16:12:25作者:伍希望

@gorhill/ubo-core 是 uBlock Origin(uBO)浏览器扩展的核心过滤引擎被封装成的独立 Node.js 包:它没有任何外部依赖,提供解析并执行静态网络过滤列表(SNFE)的能力,以及专为大规模主机名集合设计的压缩 Trie 容器 HNTrieContainer。读完本文,你将能够从零创建一个过滤引擎实例、批量加载过滤列表、对任意网络请求做匹配判定,并利用序列化("selfie")机制把解析结果缓存起来以跳过二次解析;同时理解引擎单实例约束、useLists() 内部编译流程与 WASM 加速的可选路径在源码中的具体落点。

包概览:@gorhill/ubo-core 是什么

platform/npm/README.mdplatform/npm/package.json 可以确认该包的关键事实:

  • 包名 @gorhill/ubo-core,仓库内版本为 0.1.30typemodule,即原生 ES Module 包(README 中亦有明确说明,API 仍属早期版本,可能随时变化);
  • 零运行时依赖(devDependencies 为空对象);
  • 运行环境要求 Node.js >=18.0.0、npm >=6.14.4
  • 许可证为 GPL-3.0-or-later。

它的核心定位是:把 uBO 扩展中"解析过滤列表 + 按列表规则匹配网络请求"这一静态网络过滤引擎(Static Network Filtering Engine, SNFE)原样搬进 Node.js 环境。README 特别指出,其匹配算法高度高效,_尤其_针对大规模纯主机名(pure hostname)集合做了优化——这正是过滤列表中最常见的形态(如 ||ads.example.com^ 这类纯域名屏蔽规则)。

列表来源可以是任意支持 AdBlock 风格的过滤列表(EasyList/EasyPrivacy、uBlock 自家的过滤列表),也可以是纯域名列表或 hosts 文件格式的黑名单(Block List Project、Steven Black 的 HOSTS 等)——引擎的解析器会逐行处理,非过滤行会被静默跳过。

安装与运行前提

npm install @gorhill/ubo-core

由于包采用原生 JavaScript 模块,你的项目最好也是 ES Module(在 package.json 中加入 "type": "module",仓库中的 platform/npm/package.json 自身即如此配置)。Node 版本必须不低于 18,这是 engines 字段声明的硬性要求。

使用 SNFE:从创建实例到匹配请求

README 的 Usage 章节给出了完整的最小用法,下面按"创建 → 加载列表 → 匹配请求 → 序列化"的顺序完整走一遍,并补充每一步在源码中的对应实现。

创建引擎实例

引擎的代理 API 必须这样导入:

import { StaticNetFilteringEngine } from '@gorhill/ubo-core';

如果你必须以 CommonJS/动态导入方式加载 Node.js 模块:

const { StaticNetFilteringEngine } = await import('@gorhill/ubo-core');

然后创建实例:

const snfe = await StaticNetFilteringEngine.create();

从源码实现看(platform/nodejs/index.js),StaticNetFilteringEngine 是一个包装类:构造函数中检查 snfeProxyInstance,若已存在实例则抛出 Only a single instance is supported. 错误——这解释了 README 中"目前只能存在一个 SNFE 实例"的限制。create() 是静态异步方法,它默认还会初始化 Public Suffix List(pslInit()),失败时抛出 Failed to initialize public suffix list. 错误;传入 { noPSL: true } 可跳过该步骤。释放实例则调用静态方法 StaticNetFilteringEngine.release()

加载过滤列表:useLists()

useLists() 接受一个数组,元素可以是对象或"解析为对象的 Promise"。每个对象通过 raw 属性暴露列表原始文本,name 属性(可选)记录列表名,列表如何获取完全由调用方决定:

await snfe.useLists([
    fetch('easylist').then(r => r.text()).then(raw => ({ name: 'easylist', raw })),
    fetch('easyprivacy').then(r => r.text()).then(raw => ({ name: 'easyprivacy', raw })),
]);

useLists()platform/nodejs/index.js 的实现揭示了几个重要的行为细节:

  1. 原子替换:每次调用先执行 snfe.reset() 清空全部过滤器,再整体装入新列表,即"最后一次 useLists() 生效";
  2. 并发解析:所有列表的 Promise 通过 Promise.all 并行等待,任何一个 Promise 被 reject,整个 useLists() 即抛错(platform/npm/tests/snfe.js 中有专门测试这一行为);
  3. 逐行编译:每个列表经 compileList() 处理——LineIterator 逐行迭代(支持 \ 续行),sfp.AstFilterParser 解析 AST,仅保留网络过滤规则(cosmetic 规则被 isNetworkFilter() === false 过滤掉),再由 snfe.createCompiler() 编译入 CompiledListWriter
  4. 收尾优化:所有列表装入后依次调用 snfe.freeze()snfe.optimize(),冻结并提交过滤结构。

调用方传入的对象也可携带预编译的 compiled 属性(字符串),此时引擎直接跳过解析,用 CompiledListReader 读取——这是"复用编译产物"的进阶路径。

匹配请求:matchRequest() 的返回值语义

列表加载完毕后即可对请求做匹配。README 的示例:

// Not blocked
if ( snfe.matchRequest({
    originURL: 'https://www.bloomberg.com/',
    url: 'https://www.bloomberg.com/tophat/assets/v2.6.1/that.css',
    type: 'stylesheet'
}) !== 0 ) {
    console.log(snfe.toLogData());
}

// Blocked
if ( snfe.matchRequest({
    originURL: 'https://www.bloomberg.com/',
    url: 'https://securepubads.g.doubleclick.net/tag/js/gpt.js',
    type: 'script'
}) !== 0 ) {
    console.log(snfe.toLogData());
}

// Unblocked
if ( snfe.matchRequest({
    originURL: 'https://www.bloomberg.com/',
    url: 'https://sourcepointcmp.bloomberg.com/ccpa.js',
    type: 'script'
}) !== 0 ) {
    console.log(snfe.toLogData());
}

这里值得注意返回值是三态而非布尔:

返回值 含义
0 未命中任何规则(Not blocked)
1 命中屏蔽规则(Blocked)
2 命中例外/放行规则(Unblocked)

demo.js 中即按 1/2/0 三个分支分别打印 Blocked:Unblocked:Not blocked(见 platform/npm/demo.js)。toLogData() 返回最近一次匹配的详细日志数据(命中的规则文本、来源列表等),便于排查"为什么这条请求被拦/放行"。

代理类上还暴露了更完整的 API(同样在 platform/nodejs/index.js 中定义):

  • matchAndFetchModifiers(details, modifier):仅获取某一类修饰符(如 $redirect$removeparam 的取值)的匹配结果;
  • hasQuery(details) / filterQuery(details):判断/执行 URL 修饰符(如 removeparam),filterQuery() 会返回 { redirectURL, directives }
  • isBlockImportant():判断最近一次屏蔽是否来自 $important 规则;
  • createCompiler(parser) / compileList(...):供外部自行编译列表并传回 compiled 字符串;
  • serialize() / deserialize():见下节。

请求参数 details 会被 fctx.fromDetails(details) 归一化为引擎内部使用的 FilteringContextsrc/js/filtering-context.js),示例中出现的 originURLurltype 即其核心字段;originURL 决定了 1p/3p~third-party 等基于来源的匹配判定。

序列化与反序列化:用 "selfie" 跳过二次解析

README 指出,当所有过滤列表都装入引擎后,可以把引擎内容序列化为一个 JS 字符串:

const serializedData = await snfe.serialize();

之后用这个字符串快速加载引擎内容,无需重新解析和编译列表

const snfe = await StaticNetFilteringEngine.create();
await snfe.deserialize(serializedData);

从实现看,serialize() 内部是 snfe.serialize() 得到原始数据后,再经 src/js/s14e-serializer.jss14e.serialize(data, { compress: true }) 压缩为字符串;deserialize() 则是 s14e.deserialize() 解压还原后调用 snfe.unserialize(data) 恢复引擎状态。这正是 uBO 扩展本体保存"引擎快照"的同一套机制。

这个机制在 platform/npm/demo.js 中被用成一个实用的 7 天本地缓存策略(demo 称之为 "selfie"):

const snfe = await StaticNetFilteringEngine.create();

// 检查本地缓存 cache/selfie.txt 是否在 7 天以内
let selfie;
const ageInDays = await fs.stat('cache/selfie.txt').then(stat => {
    const fileDate = new Date(stat.mtime);
    return (Date.now() - fileDate.getTime()) / (7 * 24 * 60 * 60);
}).catch(( ) => Number.MAX_SAFE_INTEGER);

if ( ageInDays <= 7 ) {
    selfie = await fs.readFile('cache/selfie.txt', { encoding: 'utf8' })
        .then(data => typeof data === 'string' && data !== '' && data)
        .catch(( ) => { });
    if ( typeof selfie === 'string' ) {
        await snfe.deserialize(selfie);
    }
}

// 缓存缺失或过期:拉取过滤列表并写入新缓存
if ( !selfie ) {
    console.log(`Fetching lists...`);
    await snfe.useLists([
        fetchList('ubo-ads', 'https://ublockorigin.github.io/uAssetsCDN/filters/filters.min.txt'),
        fetchList('ubo-badware', 'https://ublockorigin.github.io/uAssetsCDN/filters/badware.min.txt'),
        fetchList('ubo-privacy', 'https://ublockorigin.github.io/uAssetsCDN/filters/privacy.min.txt'),
        fetchList('ubo-unbreak', 'https://ublockorigin.github.io/uAssetsCDN/filters/unbreak.min.txt'),
        fetchList('ubo-quick', 'https://ublockorigin.github.io/uAssetsCDN/filters/quick-fixes.min.txt'),
        fetchList('easylist', 'https://easylist.to/easylist/easylist.txt'),
        fetchList('easyprivacy', 'https://easylist.to/easylist/easyprivacy.txt'),
        fetchList('plowe', 'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext'),
    ]);
    const selfie = await snfe.serialize();
    await fs.mkdir('cache', { recursive: true });
    await fs.writeFile('cache/selfie.txt', selfie);
}

demo 随后对三条测试请求逐一执行 snfe.matchRequest(test) 并按 1/2/0 打印结果。README 的 Usage 章节开头即指向这份 demo:"See ./demo.js in package for instructions to quickly get started"。

完整的本地体验流程如下(源自 platform/npm/demo.js 头部注释):

mkdir myproject
cd myproject
npm install @gorhill/ubo-core
cp node_modules/@gorhill/ubo-core/demo.js .
# 若项目 package.json 未声明 "type": "module",建议添加以消除 ESM 警告
node demo.js

demo 刻意保持极简,几乎不做错误处理;它先拉取列表、把引擎序列化为 ./cache/selfie.txt,之后每次运行优先复用缓存,避免重复访问远端服务器。

HNTrieContainer:面向主机名的压缩 Trie

README 的 Extras 章节介绍了包内可直接使用的底层 API 之一 HNTrieContainer——一个专门存储与查找主机名的优化压缩 Trie(compressed trie)容器。

匹配语义:从右到左

其匹配算法为域名场景专门设计:主机名的 label 从右到左匹配,因此若 Trie 中存了 example.org,查询 www.example.org 会命中;而 anotherexample.org 不会命中(label 边界不满足)。

README 给出的独立用法示例(可原样复制到项目中):

import HNTrieContainer from '@gorhill/ubo-core/js/hntrie.js';

const trieContainer = new HNTrieContainer();

const aTrie = trieContainer.createOne();
trieContainer.add(aTrie, 'example.org');
trieContainer.add(aTrie, 'example.com');

const anotherTrie = trieContainer.createOne();
trieContainer.add(anotherTrie, 'foo.invalid');
trieContainer.add(anotherTrie, 'bar.invalid');

// matches() return the position at which the match starts, or -1 when
// there is no match.

// Matches: return 4
console.log("trieContainer.matches(aTrie, 'www.example.org')", trieContainer.matches(aTrie, 'www.example.org'));

// Does not match: return -1
console.log("trieContainer.matches(aTrie, 'www.foo.invalid')", trieContainer.matches(aTrie, 'www.foo.invalid'));

// Does not match: return -1
console.log("trieContainer.matches(anotherTrie, 'www.example.org')", trieContainer.matches(anotherTrie, 'www.example.org'));

// Matches: return 0
console.log("trieContainer.matches(anotherTrie, 'foo.invalid')", trieContainer.matches(anotherTrie, 'foo.invalid'));

注意 matches() 的返回约定:命中时返回匹配起始位置www.example.orgexample.org 从第 4 位开始,故返回 4;精确命中返回 0),未命中返回 -1。这个"返回偏移量"的约定不是随手设计——从源码结构看(src/js/hntrie.js),匹配函数在确认边界匹配成功后返回的是剩余 needle 的索引 ineedle,调用方(SNFE)可以直接据此截取主机名中"剩余的前缀部分"做下一步判断,避免额外字符串操作。

README 同时强调了一个重要的生命周期约束:

reset() 方法必须用于从容器移除所有 trie,无法单独移除某一个 trie;reset 后,先前 createOne() 返回的 trie 引用(如 aTrieanotherTrie)全部失效,不应再使用。

trieContainer.reset();

底层实现:字符缓冲区 + 三槽单元格

HNTrieContainer 被设计为以 CPU 和内存效率为第一优先级存储海量主机名,是 uBO 的关键组件。从 src/js/hntrie.js 的实现可以看到其核心设计:

  • 整个容器由一段按页(PAGE_SIZE)对齐的 Uint8Arraybuf)加上视图 Uint32Arraybuf32)构成,没有逐节点对象开销;
  • 每个 trie 根节点只占 3 个 uint32 槽(前驱指针、后继指针、字符段引用),createTrie() 在需要时自动 growBuf() 扩展缓冲;
  • 主机名被切成"段"(segment)存在字符缓冲区的独立区域中,单元格中的 v 字段高位存段内字符起始偏移、低 7 位存段长、第 8 位(0x80)是"段边界"标志位——只有当该位为真、且查询串中此处正好是 . 或末尾时才判定为域名边界匹配成功(matchesJS()if ( ineedle === 0 || buf8[ineedle-1] === 0x2E ) 一行即此判定);
  • 容器还维护 lastStored 缓存:连续添加相同主机名时省去重复的 needle 拷贝。

文件末尾(src/js/hntrie.js)还有一个值得注意的机制:matchesadd 默认指向纯 JS 实现(matchesJS/addJS),但 matchesWASM/addWASM 字段预留了 WASM 加速实现位;包内自带的 hntrie.wasm 模块编译成功后可替换这两个热路径方法。构建脚本 tools/make-nodejs.sh 会把 src/js/wasm/hntrie.wasmbiditrie.wasm 等二进制序列化为 .wasm.json(字节数组的 JSON),这样 Node.js 环境下无需文件系统路径也能 WebAssembly.compile()——enableWASM() 即通过读取这些 JSON 完成加载(见 platform/nodejs/index.js)。需要说明的是,WASM 模块仅在 CPU 为小端序时启用(加载前会做端序检测),且完全可选:WASM 不可用时自动回落到 JS 实现。

测试与构建工具链

包内自带完整的测试用例,可在本地验证引擎行为是否符合预期(platform/npm/test.js):

node test.js --mocha               # 运行 tests/wasm.js 与 tests/snfe.js
node test.js --mocha --full-battery  # 追加大规模数据集测试 tests/request-data.js

测试基于 mocha 并以 --experimental-vm-modules 启动,esm-world 为每个用例创建隔离的模块世界。platform/npm/tests/snfe.js 覆盖了三块核心断言:

  • 初始化:第二次 create() 必须被 reject(验证单实例约束);
  • 列表加载:空数组、空列表、单/多过滤器、Promise 形式列表均不抛错;任何 Promise 被 reject 时整个 useLists() 抛错;
  • 匹配语义:纯主机名屏蔽规则返回 1、例外规则返回 2$important 使 isBlockImportant() 为真、$~stylesheet,all 这类类型排除规则对 stylesheet 请求返回 0localhost 不匹配 domain=... 规则等;
  • 序列化往返serialize()deserialize() 后匹配行为保持一致,包括 removeparam 修饰符在反序列化后仍正确改写 URL。

发布侧的打包流程(仅供了解,仓库只读,不建议在本仓库内执行):tools/make-npm.sh 先调用 tools/make-nodejs.sh 收集 src/js/ 下约 20 个引擎源文件与 src/lib/ 下的依赖库(csstree、punycode、publicsuffixlist 等),再复制 platform/npm/ 下的 *.json*.jstests/ 与 README,最后 npm run buildnpm pack 生成 uBlock0.npm.tgz。这解释了包内文件与仓库 src/ 的对应关系:你引用的 @gorhill/ubo-core/js/hntrie.js 即来自 src/js/hntrie.jsStaticNetFilteringEngine 的包装类定义于 platform/nodejs/index.js

小结

  • 通过 StaticNetFilteringEngine.create()(单实例)+ useLists()(原子替换、并行 Promise、逐行编译)+ matchRequest()(三态返回 0/1/2)构成最小可用的 Node.js 网络过滤判定链路;
  • serialize()/deserialize() 提供"解析一次、处处复用"的引擎快照能力,demo.js 的 7 天缓存是其典型用法;
  • HNTrieContainer 提供零对象开销的压缩 Trie,右到左的 label 匹配 + 返回偏移量的接口使其既能独立用于域名黑名单查找,也是 SNFE 内部纯主机名快速路径的核心数据结构;
  • WASM 加速是可选增强,纯 JS 实现可完整回退,测试套件(mocha)覆盖了初始化、加载、匹配与序列化往返的关键断言,可作为行为基准。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384