---
layout: post
title: 用Service Worker实现一个反向代理
tags: [浏览器, Service Worker, Worker, 反向代理]
---

  现代浏览器真是强大，可以替代一些服务器的功能了！<!--more-->   

# 起因
  前段时间在和群友聊天的时候，提到了我博客的[分发方案](/2022/02/14/move.html)，这么多年过去之后我已经在很多平台上[分发](/proxylist.html)了我的博客，不过这只是多重冗余，并不算去中心化（虽然我也有向IPFS同步，不过IPFS还得pin，也不太可靠）……所以这么看来，我的博客似乎还不算极其可靠😂？但其实不完全是这样。因为除了向不同平台的分发，我的博客还有一个全文搜索的功能。更重要的是，之前做[文章推荐功能](/2024/10/01/suggest.html)时，会把整个博客所有文章的文字存到访客浏览器的localStorage中。这么说来，只要有人访问了我博客的文章，他们的浏览器中就会保存一份我博客文章的完整文本副本。从这个角度看，可靠性应该算是相当高了吧？   
  不过我之前的分发方案里还记录了一点，在GitHub Pages以外的平台我还打包了一份全站生成后的代码，之所以要全站打包，也是希望我的博客能尽可能的分发，考虑到几乎所有的Linux发行版一定有tar，而不一定有zip，所以我最终打包成了tgz格式。如果能让访客下载这个全站打包好的副本，相比于浏览器里只存储了文章文字的全文数据，这应该是一个更好的备份方式吧？毕竟我的博客本身也是我的作品……所以这个压缩包到底有什么地方可以用到呢？   
  这时候我想起来，现代的浏览器功能已经非常强大了，甚至在浏览器里直接运行一个Web服务器也完全没问题。如果能让访客在浏览器里下载那个压缩包并运行一个Web服务器，那就相当于在他们本地设备上部署了一份我的博客副本。这样一来，除了我自己搭建的网站之外，这些访客的本地也运行着一个我的博客实例😆（当然，这份副本只有访客自己能看到）。   

# 研究实现方案
  想要在浏览器上运行Web服务器其实很简单，那就是使用Service Worker，它可以完全离线在浏览器上工作。格式的话和以前写过的Cloudflare Worker非常相似，毕竟Cloudflare Worker就是模仿Service Worker的方式运行啊😂，所以我要是想写Service Worker应该很简单。   
  有了执行的东西之后就是存储，在Service Worker上存储可以用Cache Storage，用它的话不仅可以保存文件的内容，还可以保存响应头之类的东西，用来和Service Worker配合使用非常的方便，不过既然是Cache，它的可靠性就不能保证了，浏览器很可能在需要的时候清除缓存内容，所以相比之下用IndexedDB应该会更可靠一些。   
  那么接下来就该处理我的tgz文件了，tgz的本质是tar文件被gzip压缩之后的东西。浏览器解压gzip倒是简单，可以用Compression Stream API，但它也只能处理gzip了……对于tar的处理似乎就必须用第三方库。而tar的库在网上搜了搜似乎很少，网上找了个[tarjs](https://github.com/gera2ld/tarjs)库，文档写的也看不懂，⭐️也很少，看来是有这个需求的人很少啊，而且还要用现代JS那种开发方式，要用什么npm之类的。在[上一篇文章](/2025/07/24/screenshot.html)我就说过我不是专门写前端的，对在自己电脑上安装Node.js之类的东西很反感。后来问AI也完全写不出能用的代码，估计这个功能还是太小众了……另外又想到除了这个问题之外还要处理网站更新的时候该怎么通知Service Worker之类乱七八糟的事情……所以只好作罢😅。   

# 使用Service Worker进行反向代理
  这么看来离线运行我的博客似乎有点麻烦，不过既然都研究了一下Service Worker，不如想想其他能做的事情……比如当作反向代理？虽然在浏览器上搞反向代理好像意义不是很大……但值得一试。我之前见过一个项目叫做[jsproxy](https://github.com/EtherDream/jsproxy)，它是用Service Worker实现的正向代理，这给了我一些启发。我在之前研究分发方案的时候发现了一些模仿GeoCities的复古静态网站托管平台，比如[Neocities](https://neocities.org)和[Nekoweb](https://nekoweb.org)。它们需要通过网页或API才能上传网站，不太方便使用CI/CD的方式部署。但是我又觉得它们的社区很有意思，所以想用Service Worker的方式反代到我的网站，显得我的网站是部署在它们上面一样。   
  这个做起来非常简单，其实就和我以前用[Cloudflare Worker搭建反代](/2021/03/02/workers.html#%E9%A6%96%E5%85%88%E7%BB%99%E8%87%AA%E5%B7%B1%E6%90%AD%E4%B8%AA%E5%8F%8D%E4%BB%A3)几乎完全一样，遇到请求之后直接通过Fetch获取内容然后再返回就行，唯一不同的就是浏览器存在跨域策略，在跨域时只有对应网站存在合适的响应头才可以成功请求，还好我用的Pages服务大多都允许跨域。但是在我实际测试的时候发现这个允许跨域的等级不太一样，比如GitHub Pages的响应头里包含`Access-Control-Allow-Origin: *`，但是不允许OPTIONS方式请求，另外如果要修改请求头，在响应头里还要一一允许相应的请求头才行……当然对于这种问题解决起来很简单，就和我之前写的[订阅源预览](/2025/04/08/feed.html)一样，用[cloudflare-cors-anywhere](https://github.com/Zibri/cloudflare-cors-anywhere)搭建的CORS代理就可以，有了这个就可以轻松使用Service Worker反代其他网站了。   
  当然对我来说其实有`Access-Control-Allow-Origin: *`就够了，我也不需要花里胡哨的请求方式，也不需要在请求头和请求体里加什么莫名其妙的东西，所以对我来说直接请求我的某一个镜像站就可以，于是代码如下：   
  **index.html**
```html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>Mayx的博客</title>
</head>

<body>
    <script>
        // 注册 Service Worker
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/sw.js')
                .then(registration => {
                    console.log('Service Worker 注册成功:', registration.scope);
                    // 刷新网页
                    location.reload();
                })
                .catch(error => {
                    console.error('Service Worker 注册失败:', error);
                    location="https://mabbs.github.io";
                });
        } else {
            location="https://mabbs.github.io";
        }
    </script>
    <h1>Redirecting&hellip;</h1>
    <a href="https://mabbs.github.io">Click here if you are not redirected.</a>
</body>

</html>
```
  **sw.js**
```javascript
const TARGET_SITE = '被反代的网站'; //也可以用CORS代理

self.addEventListener('install', event => {
    // 强制立即激活新 Service Worker
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', event => {
    // 立即控制所有客户端
    event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', event => {
    if (new URL(event.request.url).origin == self.location.origin) {
        event.respondWith(handleProxyRequest(event.request));
    }
});

async function handleProxyRequest(request) {
    try {
        // 构建目标 URL
        const targetUrl = new URL(request.url);
        const proxyUrl = TARGET_SITE + targetUrl.pathname + targetUrl.search;

        // 创建新请求（复制原请求属性）
        const proxyRequest = new Request(proxyUrl, {
            method: request.method,
            // headers: request.headers,
            // body: request.body
        });

        // 发送代理请求
        const response = await fetch(proxyRequest);

        // 返回修改后的响应
        return new Response(response.body, {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers
        });

    } catch (error) {
        console.error('Proxy error:', error);
        return new Response('Proxy failed', { status: 500 });
    }
}
```
  最终的实际效果： <https://mayx.nekoweb.org>

# 感想
  虽然折腾了半天没能增强我博客的可靠性……但是体会到了现代浏览器的强大之处，难怪前几年会提出ChromeOS和PWA之类的东西，原来浏览器功能还是相当强大的，用了Service Worker以后即使是纯前端也可以有和使用服务器一样的体验，在过去的浏览器中要是想实现这样的功能……好像也不是不可能😂，用AJAX加服务器使用伪静态策略其实是可以做到的……其实Service Worker的功能更多还是在离线时使用的，我这个例子好像没体现它的优势😆。   
  但总的来说相比以前想要实现这种反代的功能代码还是更清晰，也更简单了，也许以后如果有机会我又有心思让博客在访客浏览器上离线运行，那就可以体现Service Worker真正的优势了🤣。   