JS `Service Worker` `Streams API` 组合:实时代理与响应改造

(清清嗓子,敲敲麦克风)

各位观众老爷们,晚上好!我是老码,今天咱们不聊妹子,聊点硬核的——JS Service Worker 加上 Streams API,打造实时代理和响应改造的骚操作!

别怕,听着唬人,其实都是纸老虎。今天老码就用最通俗的语言,加上大量的代码示例,带你们把这俩玩意儿玩儿明白。保证你们学完之后,不仅能自己写出牛逼哄哄的代理,还能在面试的时候把面试官忽悠的一愣一愣的。

一、开胃小菜:Service Worker 是个啥?

Service Worker,你可以把它想象成你浏览器里的一个“保安大叔”。它独立于你的网页运行,专门负责拦截和处理网络请求。

  • 离线可用: 你可以缓存网页资源,让用户在没网的时候也能访问你的网站,体验嗖嗖的!
  • 消息推送: 可以给用户发送通知,比如“老码又更新文章了,快来瞅瞅!”
  • 后台同步: 可以在后台默默地同步数据,比如上传照片,发送消息啥的。

要让这个“保安大叔”上班,我们需要注册它:

// index.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker 注册成功:', registration);
    })
    .catch(error => {
      console.log('Service Worker 注册失败:', error);
    });
}

这段代码很简单,就是检查浏览器是否支持 Service Worker,如果支持,就注册 sw.js 这个文件作为 Service Worker。

重点来了,sw.js 才是我们真正写代码的地方!

一个最简单的 sw.js 看起来是这样的:

// sw.js
self.addEventListener('install', event => {
  console.log('Service Worker 安装了!');
  // 通常在这里缓存静态资源
});

self.addEventListener('activate', event => {
  console.log('Service Worker 激活了!');
  // 通常在这里清理旧的缓存
});

self.addEventListener('fetch', event => {
  console.log('Service Worker 拦截了一个请求:', event.request.url);
  // 通常在这里处理网络请求
});
  • install 事件:Service Worker 安装的时候触发。
  • activate 事件:Service Worker 激活的时候触发。
  • fetch 事件:Service Worker 拦截到网络请求的时候触发,这是我们的重头戏!

二、正餐:Streams API 是个啥?

Streams API,你可以把它想象成一个“水管”。它可以让你以流的方式处理数据,而不是一次性加载整个文件。

  • 分段处理: 可以把一个大文件分成很多小块,一块一块地处理,省内存!
  • 实时处理: 可以实时地接收和处理数据,比如直播视频,实时聊天啥的。
  • 管道操作: 可以把多个处理步骤像管道一样连接起来,让数据像流水线一样经过处理。

Streams API 主要有三种类型的流:

类型 描述
ReadableStream 可读流,用于读取数据。
WritableStream 可写流,用于写入数据。
TransformStream 转换流,用于转换数据,可以同时读取和写入数据,就像一个中间处理环节。

三、主菜:Service Worker + Streams API = 实时代理 + 响应改造

现在,我们把 Service Worker 和 Streams API 结合起来,看看能搞出什么花样!

3.1 简单代理:拦截并转发请求

// sw.js
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request) // 直接转发请求
  );
});

这段代码是最简单的代理,它拦截所有请求,然后使用 fetch API 转发到原始服务器。

3.2 响应改造:修改响应头

// sw.js
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).then(response => {
      // 修改响应头
      const newHeaders = new Headers(response.headers);
      newHeaders.set('X-Custom-Header', 'Hello from Service Worker!');

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newHeaders
      });
    })
  );
});

这段代码拦截所有请求,然后修改响应头,添加了一个 X-Custom-Header

3.3 实时代理:使用 Streams API 修改响应体

这才是真正的重头戏!我们要使用 Streams API 来实时地修改响应体,比如替换敏感词,添加水印啥的。

// sw.js
self.addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const response = await fetch(request);

  // 检查响应类型,只处理文本类型
  if (!response.headers.get('Content-Type')?.startsWith('text/')) {
    return response;
  }

  // 创建 TransformStream,用于替换敏感词
  const transformStream = new TransformStream({
    transform(chunk, controller) {
      // 将 ArrayBuffer 转换为字符串
      const decoder = new TextDecoder();
      const text = decoder.decode(chunk, { stream: true });

      // 替换敏感词
      const replacedText = text.replace(/敏感词/g, '***');

      // 将字符串转换为 Uint8Array
      const encoder = new TextEncoder();
      const encodedText = encoder.encode(replacedText);

      // 将 Uint8Array 放入队列
      controller.enqueue(encodedText);
    }
  });

  // 将响应体的 ReadableStream 通过管道传递给 TransformStream,再传递给 Response
  return new Response(response.body.pipeThrough(transformStream), {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
}

这段代码做了以下几件事:

  1. 拦截请求: fetch 事件监听器拦截所有请求。
  2. 检查响应类型: 只处理文本类型的响应。
  3. 创建 TransformStream: 创建一个 TransformStream,用于替换敏感词。
  4. 转换数据:transform 方法中,将 ArrayBuffer 转换为字符串,替换敏感词,再将字符串转换为 Uint8Array,然后放入队列。
  5. 管道操作: 使用 pipeThrough 方法将响应体的 ReadableStream 通过管道传递给 TransformStream,然后再传递给 Response

解释几个关键点:

  • TextDecoderTextEncoder 用于在 ArrayBuffer 和字符串之间进行转换。
  • controller.enqueue() 将处理后的数据放入队列,等待传递给下一个环节。
  • pipeThrough() 将一个 ReadableStream 通过管道传递给一个 TransformStreamWritableStream

3.4 进阶:缓存改造后的响应

为了提高性能,我们可以将改造后的响应缓存起来,下次直接从缓存中读取。

// sw.js
const CACHE_NAME = 'my-cache';

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        // 预缓存一些静态资源
        return cache.addAll([
          '/',
          '/index.html',
          '/style.css',
          '/script.js'
        ]);
      })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        if (cachedResponse) {
          // 从缓存中读取
          return cachedResponse;
        }

        // 没有缓存,则发起网络请求并改造响应
        return handleRequest(event.request);
      })
  );
});

async function handleRequest(request) {
  const response = await fetch(request);

  // 检查响应类型,只处理文本类型
  if (!response.headers.get('Content-Type')?.startsWith('text/')) {
    return response;
  }

  // 创建 TransformStream,用于替换敏感词
  const transformStream = new TransformStream({
    transform(chunk, controller) {
      // 将 ArrayBuffer 转换为字符串
      const decoder = new TextDecoder();
      const text = decoder.decode(chunk, { stream: true });

      // 替换敏感词
      const replacedText = text.replace(/敏感词/g, '***');

      // 将字符串转换为 Uint8Array
      const encoder = new TextEncoder();
      const encodedText = encoder.encode(replacedText);

      // 将 Uint8Array 放入队列
      controller.enqueue(encodedText);
    }
  });

  // 将响应体的 ReadableStream 通过管道传递给 TransformStream,再传递给 Response
  const transformedResponse = new Response(response.body.pipeThrough(transformStream), {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });

  // 将改造后的响应缓存起来
  caches.open(CACHE_NAME)
    .then(cache => {
      cache.put(request, transformedResponse.clone()); // 需要 clone,因为 response body 只能读取一次
    });

  return transformedResponse;
}

这段代码做了以下改进:

  1. 缓存静态资源:install 事件中,预缓存一些静态资源。
  2. 检查缓存:fetch 事件中,先检查缓存中是否有对应的响应,如果有,则直接从缓存中读取。
  3. 缓存改造后的响应:handleRequest 函数中,将改造后的响应缓存起来。
  4. response.clone() response.body 只能读取一次,所以我们需要使用 clone() 方法复制一份 response,用于缓存。

四、总结

今天我们学习了如何使用 Service Worker 和 Streams API 来打造实时代理和响应改造。

  • Service Worker: 浏览器里的“保安大叔”,负责拦截和处理网络请求。
  • Streams API: 数据处理的“水管”,可以以流的方式处理数据。
  • TransformStream: 数据转换的“中间站”,可以同时读取和写入数据。
  • pipeThrough() 连接多个流的“管道”,让数据像流水线一样经过处理。

通过这些技术,我们可以实现很多有趣的功能,比如:

  • 敏感词过滤: 实时过滤网页中的敏感词。
  • 广告拦截: 拦截网页中的广告。
  • 图片水印: 给网页中的图片添加水印。
  • A/B 测试: 根据用户 ID,返回不同的网页内容。

当然,这只是冰山一角,还有很多其他的应用场景等待你去探索。

五、作业

  1. 实现一个 Service Worker,将网页中的所有图片都加上一个边框。
  2. 实现一个 Service Worker,拦截指定网站的请求,并将响应体替换为 “Hello World!”。

好了,今天的讲座就到这里,希望大家有所收获! 如果觉得老码讲的还不错,记得点个赞,鼓励一下! 咱们下期再见! (挥手)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注