第十六章

完整实战两例

前面十五章学的都是单个零件。这一章把零件拼成两台能干活的机器:一台跑在云端(Workers),一台跑在手机 App 里(Surge / Loon 脚本)。

一、这一章学什么

一句话目标:看懂并亲手跑通两个完整实战,记住两条骨架——Workers 侧是「接请求 → 读数据 → 查存储 → 回响应」,脚本侧是「拿现场 → 改数据 → 交卷」。以后你遇到的项目,基本都是这两条骨架的放大版。

先说清一个现实问题:实战代码里有 Workers 专用的 env.LINKS(云端存储)和 Surge 专用的 $response、$done,这些东西只存在于各自的环境里,在浏览器页面里直接跑会报错。所以本章「动手试」环节用模拟对象代替它们:用一个普通对象假装成 KV 存储,用假的 $response 和 $done 假装成脚本环境。逻辑一模一样,只是换了个练习场,你可以在页面里真的把两个实战跑一遍。

二、概念

实战一:一个完整的 Workers 小项目(KV 短链接 + 访问计数)

先说它能干什么:你存一条短链,比如把网址 https://example.com 存成短名字 gh。之后别人访问 /gh,程序就把他送到真正的网址,同时偷偷给这条短链的访问次数加 1。这就是网上那些「短链接服务」的最小版本。

配套准备:在 Workers 后台绑一个 KV 命名空间(KV 就是云端的一个大柜子,专门按「名字 → 内容」存东西),绑定的名字叫 LINKS。代码里写 env.LINKS,指的就是这个柜子。

它处理两种请求:

存短链的部分:

// 从网址的问号后面取出 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

脚本本体:

(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);
点上面的「运行」,这里会显示改前和改后的 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,不受影响
点上面的「运行」,这里会显示每一步的值。

四、练一练

五道选择题,点一下选项立刻判对错。选错了会告诉你错在哪,选对了也会讲清为什么对。

已答对 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()——这是脚本侧最常见的翻车点。

本章小结:两个实战,一条骨架在云端「接请求 → 读数据 → 查存储 → 回响应」,一条骨架在手机端「拿现场 → 改数据 → 交卷」;剩下的全是规模和分支,骨架不变。

下一章:学习路线与练习 —— 把十七章串成一条复习路线,再配一套综合练习收尾。