Docsify 文档缓存问题终极解决方案:拦截请求自动添加版本号

0 阅读6分钟

📋 问题背景

在使用 Docsify 搭建文档网站时,你是否遇到过这样的情况:明明已经更新了文档内容,但用户刷新页面后看到的仍然是旧内容

这是一个典型的浏览器缓存问题,困扰着许多 Docsify 用户。本文将深入分析问题原因,并提供一个终极解决方案。

🔍 问题分析

缓存问题的根源

  1. 浏览器缓存机制:浏览器会缓存静态资源(包括 .md 文件)以提高性能
  2. CDN 缓存:如果使用 CDN 加速,CDN 节点也会缓存文件
  3. 缓存失效机制:默认情况下,浏览器只有在资源过期或 URL 变化时才会重新请求

常见解决方案的局限性

方案优点缺点
手动清除缓存简单直接用户体验差,需要用户操作
Nginx 设置 no-cache服务器端控制需要服务器配置权限,影响所有资源
文件名加 hash彻底解决需要构建工具支持,增加部署复杂度
URL 加时间戳简单有效每次都重新加载,影响性能
只加版本号版本管理清晰同一版本内更新不生效

🎯 最佳实践:版本号 + 时间戳组合方案

核心思路

拦截 Docsify 的 MD 文件请求,自动添加版本号 + 时间戳参数

README.md → README.md?v=1.1.0_202603260852

设计理念

  • 版本号:用于大版本更新,保证版本管理清晰
  • 时间戳:用于同一版本内的频繁更新,确保缓存及时失效
  • 组合使用:兼顾版本管理和缓存控制的需求

技术实现

方案 A:内联代码(适合小型项目)

优点:简单直接,无需额外文件 缺点:版本信息分散,不便于自动化

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>文档标题</title>
  <!-- 其他 head 内容 -->
</head>
<body>
  <div id="app"></div>
  
  <!-- 版本控制和请求拦截 -->
  <script>
    // 版本控制配置
    const DOC_VERSION = '1.1.0';  // 版本号
    const DOC_TIMESTAMP = '202603260852';  // 时间戳
    const DOC_CACHE_KEY = DOC_VERSION + '_' + DOC_TIMESTAMP;
    
    // 重写 XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
      let modifiedUrl = url;
      if (typeof url === 'string' && url.endsWith('.md') && !url.includes('v=')) {
        const separator = url.includes('?') ? '&' : '?';
        modifiedUrl = url + separator + 'v=' + DOC_CACHE_KEY;
      }
      // 使用 arguments 传递所有参数,避免参数丢失
      arguments[1] = modifiedUrl;
      return originalOpen.apply(this, arguments);
    };
    
    // 重写 fetch
    if (window.fetch) {
      const originalFetch = window.fetch;
      window.fetch = function(input, init) {
        let url = input;
        if (typeof input === 'string' && input.endsWith('.md') && !input.includes('v=')) {
          const separator = input.includes('?') ? '&' : '?';
          url = input + separator + 'v=' + DOC_CACHE_KEY;
        }
        return originalFetch.call(this, url, init);
      };
    }
  </script>
  
  <!-- Docsify 配置 -->
  <script>
    window.$docsify = {
      // 你的 docsify 配置
    }
  </script>
  
  <!-- 加载 Docsify -->
  <script src="//cdn.bootcdn.net/ajax/libs/docsify/4.13.1/docsify.min.js"></script>
</body>
</html>

方案 B:version.js 文件(推荐,适合中大型项目)

优点:集中管理,便于自动化,团队协作友好 缺点:需要处理自身缓存,多一个文件

步骤 1:创建 version.js 文件
// version.js
// 文档版本控制配置
const DOC_VERSION = '1.1.0';  // 版本号:发布新版本时更新
const DOC_TIMESTAMP = '202603260852';  // 时间戳:同一版本内更新时修改
const DOC_CACHE_KEY = DOC_VERSION + '_' + DOC_TIMESTAMP;
​
// 重写 XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
  let modifiedUrl = url;
  if (typeof url === 'string' && url.endsWith('.md') && !url.includes('v=')) {
    const separator = url.includes('?') ? '&' : '?';
    modifiedUrl = url + separator + 'v=' + DOC_CACHE_KEY;
  }
  arguments[1] = modifiedUrl;
  return originalOpen.apply(this, arguments);
};
​
// 重写 fetch
if (window.fetch) {
  const originalFetch = window.fetch;
  window.fetch = function(input, init) {
    let url = input;
    if (typeof input === 'string' && input.endsWith('.md') && !input.includes('v=')) {
      const separator = input.includes('?') ? '&' : '?';
      url = input + separator + 'v=' + DOC_CACHE_KEY;
    }
    return originalFetch.call(this, url, init);
  };
}
步骤 2:加载 version.js(解决自身缓存问题)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>文档标题</title>
  <!-- 其他 head 内容 -->
</head>
<body>
  <div id="app"></div>
  
  <!-- 加载版本配置和请求拦截 -->
  <script>
    // 动态创建 script 标签加载 version.js,添加时间戳防止缓存
    const script = document.createElement('script');
    script.src = 'version.js?v=' + Date.now();
    document.body.appendChild(script);
  </script>
  
  <!-- Docsify 配置 -->
  <script>
    window.$docsify = {
      // 你的 docsify 配置
    }
  </script>
  
  <!-- 加载 Docsify -->
  <script src="//cdn.bootcdn.net/ajax/libs/docsify/4.13.1/docsify.min.js"></script>
</body>
</html>

⚠️ 技术陷阱与解决方案

陷阱 1:页面空白,内容不显示

问题原因:重写 XMLHttpRequest 时参数丢失

错误代码

// ❌ 错误:显式声明参数,导致额外参数丢失
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
  return originalOpen.call(this, method, modifiedUrl, async, user, password);
}

解决方案

// ✅ 正确:使用 arguments 传递所有参数
XMLHttpRequest.prototype.open = function(method, url) {
  let modifiedUrl = url;
  // ... 修改 url
  arguments[1] = modifiedUrl;  // 只修改 url 参数
  return originalOpen.apply(this, arguments);  // 传递完整参数列表
}

陷阱 2:只重写 fetch 无效

问题原因:Docsify 4.x 默认使用 XMLHttpRequest

解决方案:同时重写 XMLHttpRequest 和 fetch,确保兼容性

陷阱 3:version.js 自身缓存

问题原因:version.js 文件本身也会被浏览器缓存

解决方案:使用动态脚本加载,添加时间戳参数

script.src = 'version.js?v=' + Date.now();

陷阱 4:执行顺序错误

问题原因:在 Docsify 加载后才重写请求方法

解决方案:在 Docsify 加载前重写请求方法

📊 效果验证

使用前

请求:README.md
状态:200 OK (from disk cache)
结果:显示旧内容

使用后

请求:README.md?v=1.1.0_202603260852
状态:200 OK
结果:显示最新内容

🚀 实际应用指南

方案选择

项目规模推荐方案原因
小型项目内联代码简单直接,无需额外文件
中大型项目version.js集中管理,便于自动化

更新流程

场景 1:发布新版本

  1. 修改版本号(如 1.1.0 → 1.2.0)
  2. 重置时间戳

场景 2:同一版本内更新

  1. 保持版本号不变
  2. 更新时间戳(如 202603260852 → 202603261000)

自动化配置

创建 GitHub Actions 工作流,自动更新时间戳:

name: Update Doc Version

on:
  push:
    paths:
      - 'docs/**/*.md'
    branches:
      - main

jobs:
  update-version:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Update timestamp
        run: |
          TIMESTAMP=$(date +%Y%m%d%H%M)
          sed -i "s/const DOC_TIMESTAMP = '.*'/const DOC_TIMESTAMP = '$TIMESTAMP'/" docs/zh/version.js
          sed -i "s/const DOC_TIMESTAMP = '.*'/const DOC_TIMESTAMP = '$TIMESTAMP'/" docs/en/version.js
      
      - name: Commit changes
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add docs/zh/version.js docs/en/version.js
          git commit -m "chore: auto-update doc timestamp" || echo "No changes"
          git push

🎓 技术深度解析

1. XMLHttpRequest 重写原理

XMLHttpRequest.prototype.open 是一个原型方法,我们通过修改它来拦截所有 XHR 请求。使用 arguments 对象可以保留所有原始参数,避免参数丢失。

2. 为什么需要同时重写 fetch?

  • 兼容性:不同浏览器和 Docsify 版本可能使用不同的请求方式
  • 未来-proof:现代浏览器和未来版本的 Docsify 可能更多使用 fetch
  • 完整性:确保所有 MD 文件请求都被拦截

3. 版本号 + 时间戳的设计哲学

  • 版本号:提供语义化的版本管理,便于团队协作和问题追踪
  • 时间戳:提供细粒度的缓存控制,确保及时更新
  • 组合使用:在性能和更新及时性之间取得最佳平衡

4. 性能影响评估

  • 正面影响:同一版本内的重复访问会使用缓存,提高性能
  • 负面影响:版本更新时会重新加载所有 MD 文件
  • 总体评估:利大于弊,用户体验得到显著改善

📝 最佳实践总结

  1. 选择合适的方案:根据项目规模选择内联代码或 version.js
  2. 正确重写请求方法:使用 arguments 传递所有参数
  3. 同时支持两种请求方式:重写 XMLHttpRequest 和 fetch
  4. 处理 version.js 自身缓存:使用动态加载 + 时间戳
  5. 配置自动化更新:集成 GitHub Actions 自动更新时间戳
  6. 监控和测试:定期检查缓存效果,确保方案有效

🔧 代码优化建议

1. 错误处理

// 增强版:添加错误处理
XMLHttpRequest.prototype.open = function(method, url) {
  try {
    let modifiedUrl = url;
    if (typeof url === 'string' && url.endsWith('.md') && !url.includes('v=')) {
      const separator = url.includes('?') ? '&' : '?';
      modifiedUrl = url + separator + 'v=' + DOC_CACHE_KEY;
    }
    arguments[1] = modifiedUrl;
    return originalOpen.apply(this, arguments);
  } catch (error) {
    console.error('[AutoScan Docs] XHR rewrite error:', error);
    return originalOpen.apply(this, arguments);
  }
};

2. 配置灵活性

// 增强版:可配置的文件扩展名
const CACHE_CONFIG = {
  extensions: ['.md', '.markdown'],
  version: '1.1.0',
  timestamp: '202603260852'
};
​
const DOC_CACHE_KEY = CACHE_CONFIG.version + '_' + CACHE_CONFIG.timestamp;
​
// 检查文件扩展名
const shouldAddVersion = (url) => {
  return CACHE_CONFIG.extensions.some(ext => url.endsWith(ext));
};

🎯 最终效果

通过本文介绍的方案,你可以:

  • ✅ 彻底解决 Docsify 文档缓存问题
  • ✅ 确保用户始终看到最新的文档内容
  • ✅ 保持良好的性能和用户体验
  • ✅ 便于版本管理和团队协作
  • ✅ 支持自动化更新流程

无论是个人项目还是企业级应用,这个方案都能为你提供一个可靠、高效的 Docsify 文档缓存解决方案。


相关资源


希望本文对你解决 Docsify 文档缓存问题有所帮助!如果你有任何问题或建议,欢迎在评论区留言。🎉