yanchang
yanchang
发布于 2026-05-15 / 62 阅读
0
0

【HomeLab】被 4M 小水管逼出来的邪道:用 JS 给图片强行“直连”

碎碎念:被 4M 带宽逼出来的“教做人”时刻

玩 VPS 的兄弟应该都懂这种痛。为了控制成本,国内云服务器 ECS 的公网带宽往往抠抠搜搜地选个 4M 或者 5M 就顶天了。平时跑跑脚本、做个内网穿透或者输出点纯文本 HTML,这几兆带宽其实跑得飞起,完全够用。

但前两天我给 Halo 博客上传了几张稍微高清一点的截图,直接“教我做人”了。4M 带宽满载也就 512 KB/s 的下载速度,一张 3MB 的大图得让访客对着屏幕干等 6 秒钟。遇到那种图文并茂的长篇大论,网页更是肉眼可见地一行行往下挤,体验简直是便秘级的。

我现在的网络架构是:这台 ECS 仅仅充当一个前台入口(绑了主域名),跑代理给公网访问、整个博客的“主力机”其实藏在后端(家里有动态公网 IP 、使用DDNS动态解析到域名的 服务器)。这就导致了一个很尴尬的局面——家庭服务器是没有80端口的,如果老老实实用 Nginx 做标准的反向代理,访客拉取图片的流量依然要去挤 ECS 那根 4M 的独木桥。

server {
    # 同时监听 IPv4 和 IPv6 的 443 端口,启用 SSL 和 HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # 公网访问的域名
    server_name www.yanchang.pw;

    # SSL 证书路径配置
    ssl_certificate     /etc/nginx/ssl/yanchang.pw.pem;
    ssl_certificate_key /etc/nginx/ssl/yanchang.pw.key;

    # 推荐的 SSL 安全参数(可选,根据实际情况添加)
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_protocols TLSv1.2 TLSv1.3;
    location / {
        # 核心:转发到家庭服务器的 DDNS 域名 + 端口
        proxy_pass https://www.******:******;

        # -------------------------------------------------
        # 关键配置说明:
        # -------------------------------------------------

        # 1. Host 传递:如果上游(家庭服务器)是按 Host 区分站点的,这里必须指定
        proxy_set_header Host www.*******.******;

        # 2. 传递真实 IP:保证家庭服务器日志里能看到访客的真实 IP,而不是 VPS 的 IP
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 3. 处理上游 HTTPS 的 SNI 问题
        # 因为家庭服务器也是 HTTPS,且绑定了域名,所以握手时必须传递正确的域名
        proxy_ssl_server_name on;
        proxy_ssl_name www.********.******;
    }
}

# 可选:将 HTTP 80 强制跳转到 HTTPS 443
server {
    listen 80;
    listen [::]:80;
    server_name www.yanchang.pw;
    return 301 https://$host$request_uri;
}

我的诉求很简单:网页文字继续走 ECS(保持地址栏域名不变),但图片必须让浏览器绕开代理,直接去我后端的大带宽 IP 拿。

那些走不通的“正路”

一开始想从应用层解

决。去 Halo 后台翻了一圈,发现 2.x 自带的“本地存储”策略非常死板,图片链接强制输出相对路径(比如 /upload/xxx.png),后台 UI 压根没给你留修改访问域名的入口。虽然搭个 MinIO 再弄个 S3 插件能完美解决,但为了几张截图去折腾一整套对象存储,实在有点杀鸡用牛刀了。

既然应用层改不了,那去代理层 Nginx 拦截 HTML 源码,用 sub_filter 强行把 /upload/ 替换成直连 IP?但转念一想,这得去改后端的配置文件,以后要是开个 gzip 压缩或者系统升个级,保不齐出什么幺蛾子。作为一名能用前端解决就绝不动后端的“懒人”,我决定把目光投向 Halo 的“代码注入”功能。

最终方案:带“免疫锁”的 JS 替换大法

找准病因就好办了。给代码加了个极其简单的判断条件(!srcset.includes),确保替换过的链接绝对不碰第二次。顺便给所有图片强行打上了 loading="lazy" 的标签,白嫖一波浏览器原生懒加载,连首屏性能都省了。

把下面这段代码直接扔到 Halo 后台的 设置 -> 代码注入 -> 全局 head 标签(或者页脚)里,一劳永逸:

JavaScript

<script>
// 等网页骨架加载完再动手
document.addEventListener('DOMContentLoaded', function() {
    
    // 1. 填入你后端大带宽机器的真实地址 (注意结尾别带斜杠)
    const DIRECT_IP = "https://*******.*****:******"; 
    // Halo 默认的图片路径前缀
    const PATH_PREFIX = "/upload/"; 

    function optimizeAndReplaceImages() {
        const images = document.querySelectorAll('img');
        
        images.forEach(img => {
            // 顺手优化:强制开启浏览器原生图片懒加载,节省首屏性能
            if (!img.hasAttribute('loading')) {
                img.setAttribute('loading', 'lazy');
            }

            // 替换标准 src (加了 startsWith 防止重复替换)
            let src = img.getAttribute('src');
            if (src && src.startsWith(PATH_PREFIX)) {
                img.src = DIRECT_IP + src;
            }
            
            // 【核心修复】正则替换 srcset 里面的路径,必须带上免疫锁防止套娃死循环
            let srcset = img.getAttribute('srcset');
            if (srcset && srcset.includes(PATH_PREFIX) && !srcset.includes(DIRECT_IP)) {
                const regex = new RegExp(PATH_PREFIX, 'g');
                img.setAttribute('srcset', srcset.replace(regex, DIRECT_IP + PATH_PREFIX));
            }

            // 兼容部分主题自带的懒加载 data-src
            let dataSrc = img.getAttribute('data-src');
            if (dataSrc && dataSrc.startsWith(PATH_PREFIX)) {
                img.setAttribute('data-src', DIRECT_IP + dataSrc);
            }
        });
    }

    // 网页加载完毕时,先立刻跑一遍
    optimizeAndReplaceImages();

    // 挂个监听器,搞定那些无限滚动下拉的动态内容
    const observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                optimizeAndReplaceImages();
            }
        });
    });
    
    observer.observe(document.body, { childList: true, subtree: true });
});
</script>

体验与总结

保存,清缓存,刷新网页。极其舒爽!

HTML 文字骨架几毫秒就从 ECS 加载完毕,随后那一堆高清大图全部绕过代理,直接走后端 IP 并发拉取。


评论