之前的文章:

  1. Cloudflare workers不完全指南"
  2. Cloudflare Workers实战(一):随心所欲操作客户端请求

书接上回,在上一篇文章里,咱们聊了怎么在请求到达服务器之前,像个“中间人”一样对它“动手动脚”。这次,咱们把视角翻转一下,看看当服务器把响应(Response)辛辛苦苦准备好,准备发回给浏览器时,我们又能怎么在半路上把它截胡,然后来一波骚操作。有了它,你就能对响应内容进行实时、动态的修改。想想看,这意味着什么?

操作响应有啥用?

你可能会问,后端返回啥,我给用户看不就完了,折腾个啥?此言差矣!修改后端响应的能力,简直是给老系统“微创手术”的神技,有时候甚至是“无创”!你根本不用去动那些陈年老代码,就能玩出新花样。比如:

  • 想给网站加个统计? 简单!在每个HTML页面返回前,刷刷刷,自动给你注入一段Google Analytics或者其他统计脚本。什么客服插件、A/B测试,同理!
  • 后端API太啰嗦? 小事一桩!后端返回一大坨JSON,里面有好多敏感信息或者前端根本用不上的字段?在边缘用Worker裁掉它,只给前端最清爽的数据。甚至还能把好几个API的结果拼在一起,伪装成一个API。
  • 想做内容本地化? 安排!根据访问用户的IP地理位置(Cloudflare会把这个信息放在request.cf.country里),动态修改页面内容,给美国用户看英文,给中国用户看中文,是不是很贴心?不过修改响应显然不是一个好主意,更好的主意应该是在请求后端前就重定向到不同的路径,比如/cn或者/us等

是不是很吊?

拿到响应并“偷窥”一下

要想操作响应,你得先拿到它。在Workers里,这事儿通常靠fetch(request)搞定。这个函数会把原始请求发出去,然后带回来一个标准的Response对象

// worker.js
export default {
  async fetch(request, env, ctx) {
    // 1. 将原始请求发往源站
    const originalResponse = await fetch(request);

    // 2. 读取响应状态码和响应头
    const statusCode = originalResponse.status; // 200, 404, 500 etc.
    const contentType = originalResponse.headers.get('Content-Type') || '';

    console.log(`后端响应状态码: ${statusCode}`);
    console.log(`后端响应类型: ${contentType}`);

    // 3. 为了演示,我们先直接返回原始响应
    return originalResponse;
  },
};

这里有个关键点,和Request对象一样,Response对象的body也是一个可读流(ReadableStream)。这玩意儿就像一次性的磁带,只能被读取一次!读完了就没了。所以,如果你想对响应体做点啥,就得先把它读出来,然后重新组装一个新的响应。

修改响应内容

改造响应的核心思想就一句话:“复制、修改、替换”。拿到原始响应,别直接在上面改,而是以它为蓝本,创建一个全新的、修改过的Response对象,最后把这个新的响应返回给浏览器。

示例一:给响应“贴标签”——修改响应头

这个最简单也最常用。比如,咱们想给所有从服务器返回的HTML页面,都强制加上一个严格的内容安全策略(CSP)头,增强网站的安全性。

// worker.js
export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const contentType = response.headers.get('Content-Type') || '';

    // 只对HTML响应进行操作
    if (contentType.includes('text/html')) {
      // 1. 克隆原始响应,这样我们才能修改它的Headers
      const newResponse = new Response(response.body, response);

      // 2. 设置新的安全头
      newResponse.headers.set(
        'Content-Security-Policy',
        "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none';"
      );
      newResponse.headers.set('X-Content-Type-Options', 'nosniff');

      return newResponse;
    }

    return response;
  },
};

示例二:给HTML页面“做手术”—— HTMLRewriter

如果只是改改响应头,那还不够刺激。真正的大杀器是修改响应体!特别是修改HTML。但HTML通常很大,如果整个读到内存里再修改,那Worker非得累死不可(虽然说跟我们免费用户没啥关系,但是太慢了也不行是不)。所以,Cloudflare给我们提供了一个神器:HTMLRewriter

这玩意儿牛逼之处在于,它是个流式解析器。你可以把它想象成一个在HTML代码流过的管道上的智能机器人,你给它设定好规则(比如“找到所有的<img>标签”),它就在代码流过的时候,实时地对匹配到的部分进行操作,整个过程根本不需要把完整的HTML加载到内存里。性能自然比等响应全部达到之后在处理快很多。

实战一下:咱们就用它来实现刚才说的,在所有HTML页面的</body>标签前,自动注入Google Analytics的跟踪代码。

// worker.js

// 定义一个处理类,用于注入脚本
class AnalyticsInjector {
  constructor(analyticsScript) {
    this.analyticsScript = analyticsScript;
  }

  // 当匹配到 </body> 元素时,在其前面追加内容
  element(element) {
    element.before(this.analyticsScript, { html: true });
  }
}

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);

    // Google Analytics 脚本
    const gaScript = `
      <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
      <script>
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'G-XXXXXXXXXX');
      </script>`;

    // 使用HTMLRewriter进行转换
    return new HTMLRewriter()
      .on('body', new AnalyticsInjector(gaScript))
      .transform(response);
  },
};

示例三:给JSON数据“瘦身”

现在前后端分离,API返回JSON是家常便饭。同样的,我们也可以在边缘对JSON进行修改。

实战一下:假设有个API返回了用户的所有信息,但我们不希望把用户的电话phone和地址address这种敏感信息暴露给前端

别问后端为啥改,问就是需求太满,没时间做!!!

// worker.js
export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const contentType = response.headers.get('Content-Type') || '';

    if (contentType.includes('application/json')) {
      // 1. 读取原始JSON响应体并解析
      // 因为响应体只能读一次,所以我们克隆一下,以防后续需要原始响应
      const originalData = await response.clone().json();

      // 2. 过滤敏感字段
      const filteredData = {
        id: originalData.id,
        name: originalData.name,
        email: originalData.email,
      };

      // 3. 创建一个新的JSON响应
      return new Response(JSON.stringify(filteredData, null, 2), {
        headers: {
          'Content-Type': 'application/json;charset=UTF-8',
        },
      });
    }

    return response;
  },
};

示例三:无中生有!

有时候我们根本不需要理会后端服务器。Worker自己就能凭空捏造一个响应出来。这在很多场景下特别有用,比如做一个健康检查的接口,或者直接从缓存里返回内容,或者处理一些非常简单的API请求,根本没必要去打扰后端老大哥。

实战一下:咱们来实现一个GET /health的接口,它会返回一个JSON,告诉别人“我还活着”。

用来监控网站健康状态不错,但是因为不直接请求后端,其实可能造成"假健康"。

// worker.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // 实现一个简单的健康检查接口
    if (url.pathname === '/health') {
      const responseData = { status: 'ok', timestamp: new Date().toISOString() };
      return new Response(JSON.stringify(responseData), {
        headers: { 'Content-Type': 'application/json' },
      });
    }

    // 其他请求正常转发
    return fetch(request);
  },
};

小结

今天我们了解了Cloudflare Workers的响应操作能力,这让我们拥有了在边缘动态“捏造”响应的超能力。总结一下核心知识点:

  1. 响应只能读一次response.body是可读流,操作前记得clone()是个好习惯。
  2. 修改响应靠新建:通过new Response(body, init)来创建修改后的响应。
  3. 修改HTML用神器HTMLRewriter是处理HTML响应的不二之选,性能高,使用方便。
  4. JSON操作很简单:先.json()解析成对象,改完再JSON.stringify()回去。
  5. 可以无中生有:Worker可以直接new Response()创建全新的响应,无需请求源站。

掌握了请求和响应的“增删改查”,你就已经掌握了Cloudflare Workers最核心、最强大的能力。毫不夸张地说,你能用它实现半个API网关的功能了。

当然,Workers的能耐远不止于此。