这一章不写能跑的代码,学的是另一种东西:模块文件的长相。Surge、Loon、REX 里那些从网上下载的“功能包”,打开看就是一个文本文件——这一章教你把这个文件从头到尾读懂。
一句话目标:会读懂一个模块文件的骨架——知道 [Rule]、[URL Rewrite]、[Script]、[MITM] 四个段落各是干什么的,并且会写 [Script] 一行里的每一个参数。
学完之后,别人发你一个模块文件,你能自己看出来:它加了什么规则、拦了什么网址、挂了什么脚本、要解密哪些域名。不用再对着一堆英文发懵。
先认识三个名字,说的是同一类东西,只是不同软件叫法不同:
Surge 的模块:文件后缀是 .sgmodule。
Loon 的插件:文件后缀是 .lpx,也可以直接给一个插件链接导入。
REX 的模块:结构和 Surge 的基本一样。
它们的共同点:一个文本文件 = 一包功能。文件里面不直接写代码,而是声明四件事——要加哪些规则、要改写哪些网址、要挂哪些脚本、要解密哪些域名。你把文件导入客户端,客户端就照着这些声明生效。
打个比方:模块文件像一张点菜单,客户端是后厨。你勾了什么菜(声明),后厨就做什么菜。菜单本身不是菜,脚本文件才是。
下面是一个最小的完整模块。带 # 开头的是中文注释行,是写给人看的,不影响功能。先整体看一遍,不用背,后面会一段一段拆开讲。
# ↓ 文件头:这几行是模块的“身份证”,给人看的 #!name=示例模块 #!desc=这段功能是干什么的(给人看的) #!author=作者名 #!homepage=https://example.com # ↓ 规则段:往配置里加分流规则,格式是 类型, 参数, 策略 [Rule] DOMAIN-SUFFIX, example.com, DIRECT # ↓ 网址重写段:把匹配的请求改个地址,或者直接拒绝 [URL Rewrite] ^https://ads\.example\.com - reject # ↓ 脚本段:挂脚本,这一行最重要,后面细讲每个参数 [Script] 去广告 = type=http-response, pattern=^https://api\.example\.com/v1/feed, script-path=feed.js, requires-body=true # ↓ 解密段:要解密 HTTPS 才能让脚本看到内容,域名写这里 [MITM] hostname = %APPEND% api.example.com
看不懂正则(就是 ^https://… 那种带符号的网址写法)没关系,前面章节讲过,这里只需要认出它在哪一段就行。
| 段落 | 干什么 | 要点 |
|---|---|---|
| #!name / #!desc | 模块名与说明 | 在文件头,客户端的模块列表里显示的就是它 |
| [Rule] | 追加分流规则 | 语法和主配置里的规则行一模一样 |
| [URL Rewrite] | 网址改写 / 拦截 | 左边是正则,右边是新地址,或者写 reject 表示直接拒绝 |
| [Script] | 声明脚本何时运行 | 最重要的一段,下面单独细讲 |
| [MITM] | 解密域名清单 | %APPEND% 表示追加、不覆盖原有清单;http-response 类脚本基本都要配它 |
[Script] 是整个模块的核心。一行就是一条脚本声明,格式是:名字 = 参数, 参数, 参数…,参数之间用逗号隔开。逐个看:
一共常见这几种:http-request=请求发出前跑;http-response=响应回来后跑;cron=定时跑;generic=手动触发跑;event=某个事件发生时跑。去广告、改返回数据这类需求,用的基本都是 http-response。
一段正则表达式。只有网址和它对上了,这个脚本才会运行,对不上就当没这回事。例外:当 type 是 cron(定时)时,这里写的不是网址,而是定时表达式(规定几点几分跑)。
JS 文件本体所在的位置,可以是一个网址,也可以是本地路径。注意:模块文件里只有这一行“地址”,真正的代码在它指向的那个 .js 文件里。
只能填 true(要)或 false(不要)。意思是:要不要让脚本拿到请求体 / 响应体(就是数据的正文内容,简称 body)。脚本想读 body,就必须写 true。但拿 body 是费性能的,没必要别开——这是最容易踩的坑,第 5 部分有专门案例。
一个字符串,作为参数交给脚本。脚本里用 $argument 把它读出来。要传多个参数时,常用 & 拼接,或者写成 JSON 格式——格式是自己约定的,写的人和脚本对好暗号就行。
单位是秒。脚本跑超过这个时间还没结束,就会被停掉,避免卡死。注意:只有部分客户端支持这个参数。
Loon 的插件比 Surge 模块多一个 [Argument] 段:它把参数做成了可以在界面上开关、填写的选项。比如“是否开启去广告”做成一个开关,“关键词”做成一个输入框。用的人不用去改那一长串 argument 字符串,友好得多。脚本里读参数的方法和 Surge 相同。
[Argument] # 下面两行会变成界面上的一个开关和一个输入框 开启去广告 = true 关键词 = 广告
REX 的模块结构与 Surge 基本同构(长得几乎一样),导入的时候按同一套段落理解就行:先找文件头,再分 [Rule]、[Script]、[MITM] 一段一段看。
模块文件不是能运行的代码,所以这一章的动手环节是认段落。还是上面那个骨架,每道题给你三行,点一点:哪一行是题目问的那一行?选完立刻判对错。
下面三行里,哪一行是 [Script] 段的那一行?
下面三行里,哪一行是 [MITM] 段的那一行?
下面三行里,哪一行带了 requires-body=true?
认一认得分:0 / 3
模块文件虽然不是代码,但可以用代码来分析它。下面这段 JS 把一段模块文本按段落切开,数出 [Rule]、[Script]、[MITM] 每段各有几行内容。点“运行”,页面会真的执行它,并把结果显示在下方。
// 先把一小段模块文本存进变量(就是上面骨架的精简版)
let 模块文本 = `#!name=示例模块
[Rule]
DOMAIN-SUFFIX, example.com, DIRECT
DOMAIN-SUFFIX, ads.com, REJECT
[Script]
去广告 = type=http-response, pattern=^https://api.example.com/feed, script-path=feed.js, requires-body=true
[MITM]
hostname = %APPEND% api.example.com`;
// 把文本按行切开:split("\n") 的意思是按“换行”拆成一行一行
let 每一行 = 模块文本.split("\n");
// 数一数某个段落里有几行内容
function 数行数(段落名) {
// 先找到段落名所在的那一行是第几行
let 开始 = 每一行.findIndex(行 => 行.trim() === 段落名);
// 从下一行开始往后收集,直到碰到下一个 [ 开头的段落为止
let 内容行 = [];
for (let i = 开始 + 1; i < 每一行.length; i++) {
if (每一行[i].startsWith("[")) break; // 碰到下一段,停
if (每一行[i].trim() !== "") 内容行.push(每一行[i]);
}
// filter 是上一章学的筛选:只留下非空行再数个数
return 内容行.filter(行 => 行.trim() !== "").length;
}
console.log("[Rule] 段有 " + 数行数("[Rule]") + " 行内容");
console.log("[Script] 段有 " + 数行数("[Script]") + " 行内容");
console.log("[MITM] 段有 " + 数行数("[MITM]") + " 行内容");
点一个选项,立刻判对错。选错了会告诉你错在哪,选对了也会讲为什么对。
[Script] 段是干什么的?
requires-body=true 是什么意思?
[MITM] 里 hostname = %APPEND% api.example.com,这个 %APPEND% 是什么意思?
模块文件头的 #!name= 是干什么的?
Loon 的插件比 Surge 模块多什么?
练一练得分:0 / 5
从网上拿到的模块别急着导入,先从头读一遍:文件头看它叫什么、作者是谁;[Rule] 看它往你的配置里加了什么规则;[URL Rewrite] 看它拦了哪些网址;[Script] 逐行看它挂了几个脚本、分别在什么时机跑;[MITM] 看它要解密哪些域名。
读懂之后改起来就有数了:比如只想关掉其中一个脚本,把那一行删掉或在行首加 # 注释掉就行,不用整个模块都不要了。
去广告的原理是:等服务器把信息流数据返回来,脚本抢先把里面的广告条目删掉,再交给 App 显示。所以它的声明有三个关键点:type=http-response(响应回来后跑)、pattern 写信息流接口的网址(只在这个接口触发)、requires-body=true(必须拿到响应体才能删里面的广告)。
[Script] # 名字 = 时机, 匹配的网址, 脚本地址, 要拿响应体 去广告 = type=http-response, pattern=^https://api\.example\.com/v1/feed, script-path=feed.js, requires-body=true [MITM] # 别忘了配这一段:不解密,脚本根本看不到 HTTPS 的内容 hostname = %APPEND% api.example.com
注意它必须和 [MITM] 配套:接口是 HTTPS 的,不解密,脚本看到的只是一团加密数据,什么都删不了。
这是新手最常遇到的“脚本不生效”:脚本里写了读 $response.body(响应体),但 [Script] 那一行没写 requires-body=true。结果是:脚本照样运行、不报错,但拿不到 body——读出来是空的,于是判断逻辑全部落空,看起来就像脚本没起作用。
排查口诀:脚本要读 body,先回 [Script] 行检查 requires-body 有没有写 true。反过来,脚本不读 body 时也别乱开 true,白白费性能。