JavaScript 教程 · 第十二章

Surge 脚本 API 详解

前面几章学的都是 JavaScript 本身。这一章开始用在真地方:Surge 里的脚本。Surge 会给脚本一整套以 $ 开头的工具,这一章就把它们一个一个认全,再动手跑两个真脚本。

一这一章学什么

一句话目标:认识 Surge 脚本的三类工具——看现场、干活、交卷,会用 $done 的几种交法,会写「改 JSON」和「定时签到」两种脚本。

学完这一章,你应该能做到:

二概念:Surge 给脚本的工具箱

Surge 的脚本不是在空白环境里跑的。脚本一被叫起来,Surge 就先把一套工具递到它手里,这些工具的名字全都以 $ 开头。按用途分成三类,先记这个三分法,后面就不乱了:

看现场的工具

$request

当前这一次请求。http-request(请求发出前)和 http-response(响应回来后)两种脚本里都有它。

  • .url —— 网址
  • .method —— 方法,比如 GET、POST
  • .headers —— 请求头,是一个对象
  • .body —— 请求体,只有脚本配置了 requires-body 时才有,没配就是空的
$response

当前这一次响应,只有 http-response 脚本里才有(请求还没发出去时,当然没有响应)。

  • .status —— 状态码,比如 200 表示成功
  • .headers —— 响应头,是一个对象
  • .body —— 响应体,是一段字符串;想当对象用,先 JSON.parse 转一下
$argument

模块里 argument= 后面写的东西,原样传给脚本,是一个字符串。里面怎么组织格式由你自己约定:可以写成 JSON,也可以写成 a=1&b=2 这种用 & 拼接的格式,脚本里自己拆开用。

干活的工具

$httpClient

在脚本里再发一个网络请求出去(比如签到脚本要自己去请求签到接口)。

  • .get(选项, 回调) —— 发 GET 请求;.post(选项, 回调) —— 发 POST 请求
  • 回调函数的签名是 (error, response, data),三个参数按顺序记:error 为空就表示成功;response.status 是状态码、response.headers 是响应头;data 是返回内容的字符串
$persistentStore

一个持久小仓库:存进去的东西,Surge 重启、手机重启都不丢,下次脚本运行还能读出来。

  • .write(值, 键) —— 存:把值存到某个键名下
  • .read(键) —— 读:按键名把值取出来
  • 只能存字符串。想存对象,先用 JSON.stringify 把对象变成字符串再存;读出来再用 JSON.parse 变回去
$notification

发一条系统通知,就是手机顶部弹出来的那种。

  • $notification.post(标题, 副标题, 内容) —— 三个参数依次是标题、副标题、正文内容
console.log

这个不是 $ 开头的,但同样重要:打日志。写下的内容在 Surge 的脚本日志里看,脚本出问题时,靠它一步步找错在哪。

交卷的工具(最重要)

$done

交卷。脚本结束时必须调用它,告诉 Surge「我处理完了」。这是本章最重要的一条规矩:不调 $done,脚本会一直挂到超时,这段时间请求被卡住,App 就一直转圈。

$done 有几种交法,交的东西不同,效果完全不同,逐个记:

$done()

什么都不改,原样放行。最常用的收尾。

$done({})

同上,交一个空对象,意思一样:不改任何东西。

$done({ body: 新字符串 })

在 http-response 脚本里替换响应体。改 JSON 就靠它:把改好的内容变回字符串,塞进 body 交卷。

$done({ headers: {...} })

替换或增改头部,把要改的请求头/响应头写在对象里。

$done({ url: 新网址 })

在 http-request 脚本里,把这次请求改发到另一个网址去。

$done({ response: { status: 200, headers: {...}, body: "..." } })

直接伪造一个响应交回去,不等真正的服务器回复。拦截请求、模拟接口时常用。

还有一种特殊情况:cron 定时脚本是「到点被 Surge 叫醒干活」的,没有请求经过,所以它没有 $request,也没有 $response,只有干活的工具和 $done。签到脚本就是这种。

三动手试:真的跑一跑

先说清一件事:$response、$done 这些工具只存在于 Surge 里面,浏览器页面里没有。所以下面的练习都是先在页面里造两个「假的」顶上,再跑真正的脚本逻辑——逻辑部分和你以后写进 Surge 的一模一样。每段代码都能点「运行」,出错了也会把错误显示出来。

练习 1:改 JSON 去广告

服务器返回的 body 里有两条内容,第一条 isAd(是不是广告)是 true。脚本要把广告筛掉,再把新 body 交卷换回去。点「运行」,看改前改后的 body 对比。

// 第一步:造假现场。浏览器里没有真的 $response 和 $done,
// 先自己造两个假的顶上,逻辑部分和真脚本一模一样。
let $response = {
  body: '{"data":[{"title":"a","isAd":true},{"title":"b","isAd":false}]}'
  // body 是服务器返回的文字:一共 2 条,
  // a 的 isAd 是 true(是广告),b 不是
};
let $done = function (结果) {
  // 真 Surge 里 $done 是交卷;
  // 这里把“交卷内容”交给页面显示出来
  显示结果(结果);
};

// 第二步:真正的脚本逻辑(写进 Surge 的就是这一段)
try {
  let obj = JSON.parse($response.body);    // 把 body 文字转成对象
  if (obj && obj.data) {
    obj.data = obj.data.filter(item => !item.isAd);  // 筛掉 isAd 为 true 的
  }
  $done({ body: JSON.stringify(obj) });    // 变回文字,交卷替换响应体
} catch (e) {
  console.log("处理失败:" + e.message);   // 出错先记日志
  $done();                                 // 出错也要交卷放行
}
点上面的「运行」,这里会显示改前和改后的 body。

练习 2:小仓库 $persistentStore 的存和取

用普通对象模拟小仓库:先 write 存一条 cookie,再 read 读出来;然后试试存一个对象——记住,要先 JSON.stringify 变成字符串才能存。(这里是模拟:刷新页面就清空;真的 $persistentStore 重启都不丢。)

// 用一个普通对象模拟 $persistentStore(小仓库)
let 仓库 = {};

let $persistentStore = {
  write: function (值, 键) { 仓库[键] = 值; },  // write(值, 键):存
  read: function (键) { return 仓库[键]; }       // read(键):取
};

// 存一条签到用的 cookie(字符串,直接存)
$persistentStore.write("session=abc123", "cookie");

// 再读出来看看
let 读到的 = $persistentStore.read("cookie");
console.log("读到的 cookie 是:" + 读到的);

// 想存对象?先用 JSON.stringify 把对象变成字符串再存
let 设置 = { 自动签到: true, 时间: "08:00" };
$persistentStore.write(JSON.stringify(设置), "设置");
console.log("存好的设置(字符串):" + $persistentStore.read("设置"));

// 读出来要用时,再用 JSON.parse 变回对象
let 读回的设置 = JSON.parse($persistentStore.read("设置"));
console.log("变回对象后,自动签到是:" + 读回的设置.自动签到);
点上面的「运行」,这里会显示存和取的结果。

练习 3:坏数据面前,包不包 try/catch 差多少

这次的 body 是坏掉的 JSON(结尾少了 }]},拼不完整)。同一份坏数据,两种写法各跑一遍:不包 try/catch 的会抛错崩掉,包了的会把错误接住、照样交卷。

// 这次的 $response 里装的是坏数据:结尾少了 }]}
// 不是合法 JSON,JSON.parse 转不动它
let $response = { body: '{"data":[{"title":"a"}' };
let $done = function () { console.log("→ $done() 被调用,请求放行"); };

// —— 写法一:不包 try/catch,直接转 ——
let obj = JSON.parse($response.body);   // 就在这一行抛错
$done({ body: JSON.stringify(obj) });   // 出错后这行执行不到,等于没交卷

// —— 写法二:同样的活,包上 try/catch ——
try {
  let obj2 = JSON.parse($response.body);
  $done({ body: JSON.stringify(obj2) });
} catch (e) {
  console.log("处理失败:" + e.message);  // 错误被接住,先记下来
  $done();                                // 再交卷放行,App 不受影响
}

说明:点「运行」时,页面会把两种写法分开各跑一遍给你看——要是连在一起跑,写法一出错就直接崩了,轮不到写法二。

点上面的「运行」,这里会显示两种写法的不同下场。

四练一练

一共 5 道题,点一个选项立刻判对错。已答对 0 / 5。

第 1 题:脚本写完后,如果不调 $done() 会怎样?
第 2 题:在 http-response 脚本里,想替换响应体,应该用哪种交法?
第 3 题:$persistentStore 里面只能存什么?
第 4 题:$httpClient.get(选项, 回调) 的回调函数,第一个参数是什么?
第 5 题:cron 定时脚本运行时,有 $request 吗?

五适用案例

案例一去广告:筛掉信息流里的广告条目

很多 App 的信息流(feed)是服务器用 JSON 下发的,广告条目常常自带标记,比如 isAd: true。写一个 http-response 脚本:把 $response.body 转成对象,用 filter 把带广告标记的条目筛掉,再 $done({ body: 新字符串 }) 交卷换回去,App 拿到的就是干净的列表。上面「练习 1」跑的就是这个套路的最小版,真用的时候把判断条件换成那个 App 实际的广告标记就行。

案例二定时签到:脚本骨架长这样

签到脚本是 cron 定时脚本:到点被叫醒,先从小仓库读出以前存好的 cookie,用 $httpClient 发签到请求,最后 $notification.post 把结果弹个通知告诉你——不用打开 App 就知道成没成。

// 定时签到脚本骨架(cron 类型脚本,没有 $request)
let cookie = $persistentStore.read("cookie");  // 读出之前存好的 cookie

if (!cookie) {
  // 没存过 cookie,先提醒自己去抓一次
  $notification.post("签到", "", "还没存 cookie,先打开一次 App");
  $done();
} else {
  $httpClient.get({
    url: "https://example.com/checkin",   // 换成你自己的签到地址
    headers: { Cookie: cookie }
  }, function (error, response, data) {   // 回调三参数:错、响应、内容
    if (error) {
      $notification.post("签到", "", "请求失败:" + error);
    } else {
      $notification.post("签到", "", "签到完成");
    }
    $done();  // 在回调里也要交卷,别漏了
  });
}

注意最后一行:请求是异步发的,$done() 要写在回调函数里面,等结果回来再交卷;写在外面的话,请求还没回来脚本就结束了。

案例三出错也要交卷:别把人家的 App 卡死

你的脚本处理的是 App 的真实流量:body 格式变了、JSON 坏了、字段不存在,随时可能出错。一旦代码抛错又没有交卷,请求就卡到超时,用户看到的就是 App 打不开、一直转圈——锅全在你的脚本上。固定套路是:整个逻辑包上 try/catch,catch 里先 console.log 记下错误,再 $done() 放行。宁可这次不改,也不能把请求卡死;日志留着,事后慢慢修。

✓本章小结

Surge 脚本就三步:看现场 → 干活 → 交卷——用 $request / $response 看,用 $httpClient、$persistentStore、$notification 干,最后用 $done 交卷收尾,一次都不能少。
下一章预告 第十三章:Loon 脚本 API 与差异——同样是 $ 开头的工具,Loon 和 Surge 哪些一样、哪些不一样。