JAVASCRIPT 教程 · 第十一章

模块文件格式
.sgmodule / Loon 插件

这一章不写能跑的代码,学的是另一种东西:模块文件的长相。Surge、Loon、REX 里那些从网上下载的“功能包”,打开看就是一个文本文件——这一章教你把这个文件从头到尾读懂。

适合:完全零基础 互动:认一认 3 题 练习:选择题 5 道 小演示:1 个可运行
1

这一章学什么

一句话目标:会读懂一个模块文件的骨架——知道 [Rule]、[URL Rewrite]、[Script]、[MITM] 四个段落各是干什么的,并且会写 [Script] 一行里的每一个参数。

学完之后,别人发你一个模块文件,你能自己看出来:它加了什么规则、拦了什么网址、挂了什么脚本、要解密哪些域名。不用再对着一堆英文发懵。

2

概念:一个文本文件 = 一包功能

先认识三个名字,说的是同一类东西,只是不同软件叫法不同:

Surge 的模块:文件后缀是 .sgmodule。

Loon 的插件:文件后缀是 .lpx,也可以直接给一个插件链接导入。

REX 的模块:结构和 Surge 的基本一样。

它们的共同点:一个文本文件 = 一包功能。文件里面不直接写代码,而是声明四件事——要加哪些规则、要改写哪些网址、要挂哪些脚本、要解密哪些域名。你把文件导入客户端,客户端就照着这些声明生效。

打个比方:模块文件像一张点菜单,客户端是后厨。你勾了什么菜(声明),后厨就做什么菜。菜单本身不是菜,脚本文件才是。

先看骨架:一个完整模块长什么样

下面是一个最小的完整模块。带 # 开头的是中文注释行,是写给人看的,不影响功能。先整体看一遍,不用背,后面会一段一段拆开讲。

示例模块.sgmodule
# ↓ 文件头:这几行是模块的“身份证”,给人看的
#!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] 一行:每个参数是什么意思

[Script] 是整个模块的核心。一行就是一条脚本声明,格式是:名字 = 参数, 参数, 参数…,参数之间用逗号隔开。逐个看:

type(类型:脚本在什么时机跑)

一共常见这几种:http-request=请求发出前跑;http-response=响应回来后跑;cron=定时跑;generic=手动触发跑;event=某个事件发生时跑。去广告、改返回数据这类需求,用的基本都是 http-response。

pattern(正则:网址匹配上才触发)

一段正则表达式。只有网址和它对上了,这个脚本才会运行,对不上就当没这回事。例外:当 type 是 cron(定时)时,这里写的不是网址,而是定时表达式(规定几点几分跑)。

script-path(脚本文件在哪)

JS 文件本体所在的位置,可以是一个网址,也可以是本地路径。注意:模块文件里只有这一行“地址”,真正的代码在它指向的那个 .js 文件里。

requires-body(要不要把内容交给脚本)

只能填 true(要)或 false(不要)。意思是:要不要让脚本拿到请求体 / 响应体(就是数据的正文内容,简称 body)。脚本想读 body,就必须写 true。但拿 body 是费性能的,没必要别开——这是最容易踩的坑,第 5 部分有专门案例。

argument(传给脚本的参数)

一个字符串,作为参数交给脚本。脚本里用 $argument 把它读出来。要传多个参数时,常用 & 拼接,或者写成 JSON 格式——格式是自己约定的,写的人和脚本对好暗号就行。

timeout(超时秒数)

单位是秒。脚本跑超过这个时间还没结束,就会被停掉,避免卡死。注意:只有部分客户端支持这个参数。

Loon 多一段,REX 基本一样

Loon 的插件比 Surge 模块多一个 [Argument] 段:它把参数做成了可以在界面上开关、填写的选项。比如“是否开启去广告”做成一个开关,“关键词”做成一个输入框。用的人不用去改那一长串 argument 字符串,友好得多。脚本里读参数的方法和 Surge 相同。

Loon 插件多出的 [Argument] 段(示意)
[Argument]
# 下面两行会变成界面上的一个开关和一个输入框
开启去广告 = true
关键词 = 广告

REX 的模块结构与 Surge 基本同构(长得几乎一样),导入的时候按同一套段落理解就行:先找文件头,再分 [Rule]、[Script]、[MITM] 一段一段看。

3

动手试:认一认

模块文件不是能运行的代码,所以这一章的动手环节是认段落。还是上面那个骨架,每道题给你三行,点一点:哪一行是题目问的那一行?选完立刻判对错。

认一认 · 第 1 题

下面三行里,哪一行是 [Script] 段的那一行?

认一认 · 第 2 题

下面三行里,哪一行是 [MITM] 段的那一行?

认一认 · 第 3 题

下面三行里,哪一行带了 requires-body=true?

认一认得分:0 / 3

小演示:用 JS 把模块文本切开数一数

模块文件虽然不是代码,但可以用代码来分析它。下面这段 JS 把一段模块文本按段落切开,数出 [Rule]、[Script]、[MITM] 每段各有几行内容。点“运行”,页面会真的执行它,并把结果显示在下方。

数一数.js
// 先把一小段模块文本存进变量(就是上面骨架的精简版)
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]") + " 行内容");
点一下,在下面看输出结果
运行输出
4

练一练:5 道选择题

点一个选项,立刻判对错。选错了会告诉你错在哪,选对了也会讲为什么对。

第 1 题

[Script] 段是干什么的?

第 2 题

requires-body=true 是什么意思?

第 3 题

[MITM] 里 hostname = %APPEND% api.example.com,这个 %APPEND% 是什么意思?

第 4 题

模块文件头的 #!name= 是干什么的?

第 5 题

Loon 的插件比 Surge 模块多什么?

练一练得分:0 / 5

5

适用案例:这章知识用在哪

案例一:读懂一个现成模块,再动手改它

从网上拿到的模块别急着导入,先从头读一遍:文件头看它叫什么、作者是谁;[Rule] 看它往你的配置里加了什么规则;[URL Rewrite] 看它拦了哪些网址;[Script] 逐行看它挂了几个脚本、分别在什么时机跑;[MITM] 看它要解密哪些域名。

读懂之后改起来就有数了:比如只想关掉其中一个脚本,把那一行删掉或在行首加 # 注释掉就行,不用整个模块都不要了。

案例二:去广告模块的 [Script] 声明长什么样

去广告的原理是:等服务器把信息流数据返回来,脚本抢先把里面的广告条目删掉,再交给 App 显示。所以它的声明有三个关键点:type=http-response(响应回来后跑)、pattern 写信息流接口的网址(只在这个接口触发)、requires-body=true(必须拿到响应体才能删里面的广告)。

去广告模块的 [Script] 段
[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 的,不解密,脚本看到的只是一团加密数据,什么都删不了。

案例三:requires-body 没开,读 $response.body 会怎样

这是新手最常遇到的“脚本不生效”:脚本里写了读 $response.body(响应体),但 [Script] 那一行没写 requires-body=true。结果是:脚本照样运行、不报错,但拿不到 body——读出来是空的,于是判断逻辑全部落空,看起来就像脚本没起作用。

排查口诀:脚本要读 body,先回 [Script] 行检查 requires-body 有没有写 true。反过来,脚本不读 body 时也别乱开 true,白白费性能。