完整实战两例
前面十五章学的都是单个零件。这一章把零件拼成两台能干活的机器:一台跑在云端(Workers),一台跑在手机 App 里(Surge / Loon 脚本)。
一、这一章学什么
一句话目标:看懂并亲手跑通两个完整实战,记住两条骨架——Workers 侧是「接请求 → 读数据 → 查存储 → 回响应」,脚本侧是「拿现场 → 改数据 → 交卷」。以后你遇到的项目,基本都是这两条骨架的放大版。
env.LINKS(云端存储)和 Surge 专用的 $response、$done,这些东西只存在于各自的环境里,在浏览器页面里直接跑会报错。所以本章「动手试」环节用模拟对象代替它们:用一个普通对象假装成 KV 存储,用假的 $response 和 $done 假装成脚本环境。逻辑一模一样,只是换了个练习场,你可以在页面里真的把两个实战跑一遍。
二、概念
实战一:一个完整的 Workers 小项目(KV 短链接 + 访问计数)
先说它能干什么:你存一条短链,比如把网址 https://example.com 存成短名字 gh。之后别人访问 /gh,程序就把他送到真正的网址,同时偷偷给这条短链的访问次数加 1。这就是网上那些「短链接服务」的最小版本。
配套准备:在 Workers 后台绑一个 KV 命名空间(KV 就是云端的一个大柜子,专门按「名字 → 内容」存东西),绑定的名字叫 LINKS。代码里写 env.LINKS,指的就是这个柜子。
它处理两种请求:
POST /api/add?key=gh,请求体(body)里放目标网址 → 存一条短链。GET /gh→ 找到目标网址,跳转过去,并给计数 +1。
存短链的部分:
// 从网址的问号后面取出 key 的值,这里是 "gh"
const key = url.searchParams.get("key");
// 把请求体当纯文本读出来,这里是目标网址
const target = await request.text();
// 两个参数只要缺一个,就回 400(意思是:你参数没给全)
if (!key || !target) {
return new Response("参数缺失", { status: 400 });
}
// 把这条短链存进 KV 柜子:名字是 key,内容是目标网址
await env.LINKS.put(key, target);
// 回一个 JSON 结果,告诉对方存好了
return Response.json({ ok: true, key });
访问短链、跳转并计数的部分:
// 按 key 去 KV 里取出目标网址
const target = await env.LINKS.get(key);
// 读这条短链现在的计数。计数单独存一份,
// 名字是 "count:" 加上 key,比如 "count:gh"
const n = Number(await env.LINKS.get("count:" + key)) || 0;
// 计数加 1 再存回去。注意:KV 里存的全是字符串,
// 所以要用 String() 把数字变回文字再存
await env.LINKS.put("count:" + key, String(n + 1));
// 302 跳转:把访客送到真正的网址去
return Response.redirect(target, 302);
别小看这个例子。把它看懂,五个基本功就全过了一遍:路由(按网址路径分发到不同处理)、参数(从问号后面取 key)、请求体(读 body 里的网址)、KV 读写(put 存、get 取)、状态码(400 是参数错、302 是跳转)。你的 tg-bot、导航站后端,本质上都是这个骨架的放大版——请求种类更多、分支更多,但骨架没变。
实战二:一个通用的 Surge / Loon 脚本(给响应 JSON 打标记)
这个脚本干的事:某个 App 向服务器要数据,服务器回了一包 JSON。脚本在数据递给 App 之前插一手:给这包数据加一个自己的标记字段,顺手删掉一个不需要的字段。这就是很多模块「改响应」的最小写法。
先要在模块的 [Script] 段里声明一行,告诉 Surge/Loon:什么时机、用哪条规则、跑哪个脚本。
标记 = type=http-response, pattern=^https://api\.example\.com/user, script-path=本脚本, requires-body=true
type=http-response:在「响应回来的路上」动手。pattern=...:只拦这个网址的响应,其他的不管。script-path=本脚本:要跑的脚本文件。requires-body=true:我需要拿到响应的 body(正文),不声明就拿不到。
脚本本体:
(function () {
try {
// $response 是环境递进来的现场,.body 是响应的正文(一段文字)
// JSON.parse 把这段文字变成可以改的对象;body 为空时用 "{}" 兜底
const obj = JSON.parse($response.body || "{}");
// 给对象加一个自己的字段,当作"我来过"的标记
obj._patchedBy = "my-module";
// 如果不是会员(vip 是 false)并且带着试用数据,
// 就顺手把试用横幅的数据删掉
if (obj.vip === false && obj.trial) {
delete obj.trial;
}
// 把改好的对象变回文字,交给 $done 交卷,替换掉原来的 body
$done({ body: JSON.stringify(obj) });
} catch (e) {
// 出错了:记一行日志方便排查……
console.log("[标记脚本] " + e.message);
// ……然后照样交卷(不带修改),失败放行,永不拦路
$done();
}
})();
记住这个脚本的两条规矩:第一,不管成功失败,最后都要调用 $done() 交卷,不交卷,App 就会一直等到超时,看起来像网络坏了。第二,改数据三步走:JSON.parse 变对象 → 改字段 → JSON.stringify 变回去,顺序不能乱。
两例合起来,就是全部基本功
Workers 侧(云端):接请求 → 读数据 → 查存储 → 回响应
脚本侧(手机端):拿现场 → 改数据 → 交卷
真实项目剩下的变化,只是规模变大、分支变多,骨架永远是这两条。看到一个新项目先认骨架,就不会被几百行代码吓住。
三、动手试
下面三段代码都能真跑。点「运行」,页面会执行和代码框里一模一样的逻辑,把每一步的结果打印在下方。
练习 1:用普通对象模拟 KV,跑一遍实战一
这里用一个普通对象假装成 KV 柜子:get 就是按名字取东西(没有就回 null),put 就是按名字存东西。然后跑完整流程:存短链 gh → example.com,再访问 gh 两次,看计数 0 → 1 → 2。
// 假 KV:一个对象 + get / put 两个方法
const store = {};
const LINKS = {
get: (k) => (k in store ? store[k] : null), // 取不到回 null,和真 KV 一样
put: (k, v) => { store[k] = String(v); } // 存进去的全是字符串
};
// 第一步:存短链 gh → example.com
LINKS.put("gh", "https://example.com");
打印("已存入短链 gh → https://example.com");
// 第二步:访问 gh,跳转并计数
function 访问(key) {
const target = LINKS.get(key); // 取目标网址
const n = Number(LINKS.get("count:" + key)) || 0; // 读旧计数,没有就是 0
LINKS.put("count:" + key, String(n + 1)); // +1 存回去
打印("跳转到 " + target + ",这是第 " + (n + 1) + " 次访问");
}
访问("gh"); // 第一次:计数 0 → 1
访问("gh"); // 第二次:计数 1 → 2
打印("最后 KV 里存的是:" + JSON.stringify(store));
练习 2:用假的 $response 和 $done,跑一遍实战二
这里假装服务器回了一包数据:{"vip":false,"trial":{"text":"试用"}}(不是会员,还带着试用横幅数据)。跑一遍打标记逻辑,看改前改后的 body 对比:多了 _patchedBy,少了 trial。
// 假的现场:$response 是服务器回来的响应
const $response = { body: '{"vip":false,"trial":{"text":"试用"}}' };
// 假的 $done:不真交卷,只把交卷内容记下来给我们看
let 交卷内容 = null;
function $done(result) { 交卷内容 = result || {}; }
打印("改之前:" + $response.body);
// —— 下面就是实战二的脚本逻辑,一字不差 ——
try {
const obj = JSON.parse($response.body || "{}"); // 文字变对象
obj._patchedBy = "my-module"; // 加标记字段
if (obj.vip === false && obj.trial) { // 不是会员且有试用数据
delete obj.trial; // 删掉试用横幅数据
}
$done({ body: JSON.stringify(obj) }); // 变回文字,交卷
} catch (e) {
打印("[标记脚本] 出错:" + e.message);
$done(); // 失败也交卷,放行
}
打印("改之后:" + 交卷内容.body);
练习 3:看 Number(null) || 0 的兜底效果
实战一里计数第一次读的时候,KV 里根本没有这条记录,get 回来的是 null。这一行就是专门处理这个情况的:把它拆开一步一步看。
// 第一次读计数:KV 里没有,get 回来是 null
const 读到的 = null;
打印("get 回来的是:" + 读到的); // null
打印("Number(null) 是:" + Number(读到的)); // 0
打印("Number(null) || 0 是:" + (Number(读到的) || 0)); // 0
// 对比一下:已经有计数时,读到的是字符串 "5"
打印('Number("5") || 0 是:' + (Number("5") || 0)); // 5,不受影响
四、练一练
五道选择题,点一下选项立刻判对错。选错了会告诉你错在哪,选对了也会讲清为什么对。
五、适用案例
1你的 tg-bot 和导航后端,就是实战一的放大版
tg-bot 收到一条消息,本质就是「接请求」;它去 KV 里查设置、查保管箱,就是「查存储」;最后把回复发回去,就是「回响应」。导航站后端也一样:浏览器来要书签数据,后端查 KV 再回 JSON。你今天看懂的短链接骨架,把请求种类从 2 种加到 20 种,就是它们的样子。下次看它们的代码,先在心里把每段对到骨架的哪一步,就不会晕。
2给实战一加一个查询接口:/api/stat?key=gh
计数已经存好了,再加一个只查不改的接口就能看数据:收到 GET /api/stat?key=gh,取出 count:gh,用 Response.json 回一个 { key: "gh", count: 2 } 就行。这正是实战一骨架的复用:接请求 → 读参数 → 查存储 → 回响应,一步不多、一步不少。以后想给自己的服务加任何「查一下」的功能,都是这个套路。
3用实战二改一个字段,去 App 日志里验货
脚本写完别光看代码,要验货:让脚本改一个看得见的字段(比如把昵称后面加个标记),然后打开 Surge/Loon 的日志,确认三件事:脚本被触发了、日志里没有报错、$done 正常交卷了(App 数据能正常刷新就是交卷成功的证据)。如果 App 卡住不动,第一嫌疑就是忘了 $done()——这是脚本侧最常见的翻车点。
本章小结:两个实战,一条骨架在云端「接请求 → 读数据 → 查存储 → 回响应」,一条骨架在手机端「拿现场 → 改数据 → 交卷」;剩下的全是规模和分支,骨架不变。
下一章:学习路线与练习 —— 把十七章串成一条复习路线,再配一套综合练习收尾。