yanchang
yanchang
发布于 2026-05-15 / 13 阅读
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 的独木桥。

我的诉求很简单:网页文字继续走 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://yanchang.cc:8091"; 
    // 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 并发拉取。


评论