记录一下从发现问题到写完代码的过程,顺便开源给需要的人。


为什么做这个

我经常在电脑上用小红书网页版,有时候看到教程类视频想存下来反复看,或者觉得某个视频有价值想离线保存。但网页版没有下载按钮,右键另存也不管用——视频地址藏在 CDN 和动态加载逻辑里,浏览器根本拿不到直接的视频链接。

网上有不少第三方下载工具,但我没找到可靠的 Chrome 扩展。反正自己会写代码,干脆做一个。

花了大概两个月的业余时间,这个扩展现在能用了。自动检测页面视频、一键下载、批量操作,还支持去水印。代码开源在 GitHub:

🔗 https://github.com/hohband/xhs-video-helper


技术方案的选择

一开始想了几条路:

方案好处坏处
Chrome 扩展装一次就能一直用,体验好要适配 Manifest V3 的新规则
油猴脚本开发快功能受限,做不到完整的下载管理
独立网站不需要安装需要服务器,我没那个精力维护

Chrome 扩展最合适。另外我选了 Manifest V3,虽然比 V2 麻烦不少,但 V2 已经在淘汰了,不如一步到位。


怎么把视频从页面里抓出来

这是最核心的问题。试了一圈发现单一方案靠不住,小红书的视频加载方式有好几种,最后搞了三套机制互为补充:

1. Performance API 监听

浏览器加载资源时,PerformanceObserver 能捕获所有网络请求的性能记录。视频 CDN 的请求(.mp4、.m3u8、特定域名)会出现在这里。

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (isVideoUrl(entry.name)) {
      videoUrls.add(entry.name);
    }
  }
});
observer.observe({ entryTypes: ["resource"] });

好处是纯监听不干扰页面,坏处是如果视频用到懒加载,可能漏掉。


2. XHR 和 Fetch 拦截

小红书展示视频之前会先调 API,视频地址在返回的 JSON 里。通过重写 XMLHttpRequestfetch,可以从响应数据里把视频地址直接拿出来。

const originalFetch = window.fetch;
window.fetch = async (...args) => {
  const response = await originalFetch.apply(this, args);
  if (isXhsApi(args[0])) {
    response.clone().json().then(data => {
      extractVideoFromApi(data);
    });
  }
  return response;
};

好处是能拿到最原始的视频地址,很多时候是无水印版本。坏处是依赖小红书 API 的结构,平台一改接口就得跟着改。


3. DOM 扫描兜底

最简单粗暴的方式——把页面上所有 <video> 标签遍历一遍,拿 srccurrentSrc

document.querySelectorAll("video").forEach(video => {
  const url = video.src || video.currentSrc;
  if (url) videoUrls.add(url);
});

虽然拿到的 URL 可能带水印参数,但至少保证不会漏。


三套机制都用上之后,覆盖率基本没问题了。


SPA 路由切换的麻烦

小红书是单页应用,切换笔记时页面不全量刷新,PerformanceObserver 也不会重新触发。所以需要监听路由变化:

const originalPushState = history.pushState;
history.pushState = function (...args) {
  originalPushState.apply(this, args);
  setTimeout(detectVideos, 1000);
};

再加上 popstatehashchange 事件,基本覆盖了所有路由切换场景。


去水印

小红书视频的 URL 里带一堆参数,有些跟水印有关。对比分析了几十个 URL 之后,整理出这些需要去掉的参数:

const watermarkParams = ["watermark", "wm", "_wm", "logo"];
watermarkParams.forEach(p => url.searchParams.delete(p));

效果因视频而异,有些去完参数后水印确实明显少了。


Manifest V3 踩的坑

如果用 V2 做这个项目会简单很多。V3 强制用 Service Worker 替代 Background Page,但 Service Worker 里没有 DOM,所以视频检测和 UI 注入必须放在 Content Script 里,Background 只做下载管理和消息转发。

三个组件之间的通信链条拉长了,调试也更麻烦。比如视频检测在 Content Script 里跑,要先发消息给 Background,Background 才能调用 chrome.downloads.download,异步流程一多就容易出错。

还有 webRequest 在 V3 里被限制了,好在我这个场景只是监听不修改请求,影响不算大。


开源

工具本身解决的是我自己的需求,但估计也有其他人需要。MIT 协议随便用随便改,已经在 GitHub 上开源了:

🔗 https://github.com/hohband/xhs-video-helper

如果觉得有用,欢迎给个 Star ⭐,有问题也可以提 Issue。


总结

做这个扩展的过程没有什么特别高深的技术,主要就是在调试和试错。唯一想说的是,浏览器扩展开发虽然调试成本比普通前端项目高,但做出来的东西体验确实好——用户装一次就能一直用。


免责声明:本工具仅用于个人学习和研究目的,请尊重版权,合理使用。

1
0