Skip to content

前端老项目发版更新缓存解决方案

概述

在前端老项目中,由于浏览器缓存机制,用户可能无法及时获取到最新的JavaScript文件,导致功能异常或显示旧版本内容。本文档提供了三种主要的解决方案,帮助开发者在不使用现代打包工具的情况下有效解决缓存问题。

目录

  1. 配置Nginx不缓存JS文件
  2. 动态给JS文件添加版本号
  3. 构建工具升级方案
  4. 最佳实践建议

1. 配置Nginx不缓存JS文件

1.1 基本配置

通过修改Nginx配置文件,可以设置特定文件类型的缓存策略。以下是针对JavaScript文件的缓存控制配置:

nginx
server {
    listen 80;
    server_name your-domain.com;
    root /var/www/html;
    
    # 针对JS文件设置不缓存
    location ~* \.js$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
        
        # 可选:添加CORS头部
        add_header Access-Control-Allow-Origin "*";
    }
    
    # 其他静态资源可以设置较长的缓存时间
    location ~* \.(css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # HTML文件设置短缓存或不缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache, must-revalidate";
        expires 0;
    }
}

1.2 高级配置选项

1.2.1 基于环境的缓存策略

nginx
# 开发环境:完全不缓存
location ~* \.js$ {
    if ($http_host ~* "dev\.|localhost") {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }
}

# 生产环境:短时间缓存
location ~* \.js$ {
    if ($http_host !~* "dev\.|localhost") {
        add_header Cache-Control "public, max-age=300"; # 5分钟缓存
    }
}

1.2.2 基于文件路径的精细控制

nginx
# 应用核心文件不缓存
location ~* ^/js/(app|main|core)\.js$ {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

# 第三方库可以长时间缓存
location ~* ^/js/vendor/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

1.3 配置验证

配置完成后,可以通过以下方式验证:

bash
# 检查Nginx配置语法
nginx -t

# 重新加载配置
nginx -s reload

# 使用curl测试缓存头部
curl -I http://your-domain.com/js/app.js

预期响应头部应包含:

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

1.4 优缺点

优点:

  • 配置简单,无需修改代码
  • 立即生效,无需重新部署
  • 适合临时解决缓存问题

缺点:

  • 每次请求都重新下载JS/CSS,影响性能
  • 增加服务器带宽消耗
  • 不是最佳实践

2. 动态给JS文件添加版本号

2.1 add-version.js 脚本介绍

add-version.js 是一个Node.js脚本,用于自动为HTML文件中的JavaScript引用添加版本号参数,有效解决浏览器缓存问题。

2.2 脚本功能特性

  • ✅ 自动识别HTML中的本地JS文件引用
  • ✅ 支持时间戳和自定义版本号
  • ✅ 智能跳过外部CDN链接
  • ✅ 自动处理已有版本号的更新
  • ✅ 创建备份文件保护原始内容
  • ✅ 详细的操作日志输出

2.3 安装和使用

2.3.1 基本使用方法

bash
# 使用时间戳版本号(默认)
node add-version.js index.html

# 使用自定义版本号
node add-version.js -v 1.2.3 index.html
node add-version.js --version v2.0.1 index.html

# 处理多个文件
node add-version.js -v 1.0.0 *.html

2.3.2 命令行参数说明

参数简写说明示例
--version-v指定自定义版本号-v 1.2.3
--help-h显示帮助信息-h
文件路径-HTML文件路径(可选,默认index.html)dist/index.html

2.4 脚本工作原理

2.4.1 处理前后对比

处理前:

html
<script src="js/app.js"></script>
<script src="js/utils.js?v=old123"></script>
<script src="https://cdn.example.com/lib.js"></script>

处理后:

html
<script src="js/app.js?v=1757267890123"></script>
<script src="js/utils.js?v=1757267890123"></script>
<script src="https://cdn.example.com/lib.js"></script>

2.4.2 版本号生成策略

  1. 时间戳版本号(默认)

    • 格式:Date.now() 生成的13位时间戳
    • 优点:确保每次执行都是唯一值
    • 适用:开发和测试环境
  2. 自定义版本号

    • 格式:用户指定的任意字符串
    • 优点:语义化版本控制
    • 适用:生产环境发布

2.5 集成到构建流程

2.5.1 npm scripts 集成

package.json 中添加脚本:

json
{
  "scripts": {
    "build": "node add-version.js",
    "build:prod": "node add-version.js -v $(date +%Y%m%d%H%M%S)",
    "version:update": "node add-version.js -v",
    "deploy": "npm run build:prod && rsync -av ./ user@server:/var/www/html/"
  }
}

2.5.2 Git Hooks 集成

创建 .git/hooks/pre-commit 文件:

bash
#!/bin/sh
# 在提交前自动更新版本号
node add-version.js -v $(git rev-parse --short HEAD)
git add *.html

2.5.3 CI/CD 集成示例

GitHub Actions:

yaml
name: Deploy
on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
    - name: Add version to JS files
      run: node add-version.js -v ${{ github.sha }}
    - name: Deploy to server
      run: |
        # 部署脚本

2.6 高级配置

2.6.1 批量处理脚本

创建 batch-version.js

javascript
const fs = require('fs');
const path = require('path');
const VersionAdder = require('./add-version.js');

// 批量处理多个HTML文件
const htmlFiles = [
  'index.html',
  'admin.html',
  'mobile.html'
];

const version = process.argv[2] || Date.now().toString();

htmlFiles.forEach(file => {
  if (fs.existsSync(file)) {
    console.log(`\n处理文件: ${file}`);
    const versionAdder = new VersionAdder(file, version);
    versionAdder.run();
  }
});

2.6.2 配置文件支持

创建 version.config.js

javascript
module.exports = {
  // 要处理的HTML文件
  htmlFiles: ['index.html', 'admin.html'],
  
  // 版本号生成策略
  versionStrategy: 'timestamp', // 'timestamp' | 'git-hash' | 'custom'
  
  // 自定义版本号
  customVersion: '1.0.0',
  
  // 排除的文件模式
  excludePatterns: [
    'https://',
    'http://',
    '//cdn.',
    '/vendor/'
  ],
  
  // 备份设置
  backup: {
    enabled: true,
    suffix: '.backup'
  }
};

2.7 优缺点

优点:

  • 保持缓存优势,只有更新时才重新下载
  • 不影响现有代码结构
  • 可以精确控制版本号
  • 适合渐进式升级

缺点:

  • 需要手动或半自动执行
  • 需要修改部署流程
  • 版本号管理需要额外考虑

3. 构建工具升级方案

3.1 从Gulp升级到现代构建工具

3.1.1 现状分析

Gulp的局限性:

  • 配置复杂,需要手动管理依赖
  • 缺乏内置的模块化支持
  • 热更新功能有限
  • 缓存处理需要额外插件

升级的必要性:

  • 更好的开发体验
  • 自动化的缓存处理
  • 现代JavaScript特性支持
  • 更快的构建速度

3.2 Vite 迁移方案

3.2.1 Vite 优势

  • ⚡ 极快的冷启动
  • 🔥 即时热模块替换(HMR)
  • 📦 内置文件指纹(hash)
  • 🎯 按需编译
  • 🛠️ 丰富的插件生态

3.2.2 迁移步骤

1. 安装Vite

bash
npm install --save-dev vite

2. 创建 vite.config.js

javascript
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  // 多页面应用配置
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        admin: resolve(__dirname, 'admin.html')
      }
    },
    // 文件名包含hash
    assetsDir: 'assets',
    // 自定义文件名格式
    chunkFileNames: 'js/[name]-[hash].js',
    entryFileNames: 'js/[name]-[hash].js',
    assetFileNames: (assetInfo) => {
      const info = assetInfo.name.split('.');
      const ext = info[info.length - 1];
      if (/\.(css)$/.test(assetInfo.name)) {
        return `css/[name]-[hash].${ext}`;
      }
      return `assets/[name]-[hash].${ext}`;
    }
  },
  
  // 开发服务器配置
  server: {
    port: 3000,
    open: true,
    // 代理API请求
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
});

3. 更新 package.json

json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

4. 项目结构调整

project/
├── index.html          # 入口HTML文件
├── admin.html          # 管理后台入口
├── src/
│   ├── js/
│   │   ├── main.js     # 主入口文件
│   │   └── admin.js    # 管理后台入口
│   ├── css/
│   └── assets/
├── public/             # 静态资源
└── vite.config.js

3.2.3 HTML文件调整

调整前(传统方式):

html
<script src="js/app.js"></script>
<script src="js/utils.js"></script>

调整后(Vite方式):

html
<script type="module" src="/src/js/main.js"></script>

3.3 Webpack 迁移方案

3.3.1 Webpack 优势

  • 🔧 高度可配置
  • 📦 强大的代码分割
  • 🎯 精确的缓存控制
  • 🔌 丰富的loader和plugin生态

3.3.2 基础配置

1. 安装依赖

bash
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin mini-css-extract-plugin

2. 创建 webpack.config.js

javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',
  
  entry: {
    main: './src/js/main.js',
    admin: './src/js/admin.js'
  },
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js',
    clean: true
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[name].[contenthash:8][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'index.html',
      chunks: ['main']
    }),
    new HtmlWebpackPlugin({
      template: './admin.html',
      filename: 'admin.html',
      chunks: ['admin']
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    })
  ],
  
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  
  devServer: {
    static: './dist',
    port: 3000,
    open: true,
    hot: true
  }
};

3.4 构建工具对比

特性GulpViteWebpack
学习曲线中等简单复杂
构建速度中等极快较慢
热更新需配置内置需配置
文件指纹需插件内置内置
代码分割手动自动强大
配置复杂度
生态系统成熟快速发展最丰富
适用场景传统项目现代开发复杂应用

3.5 缓存处理对比

3.5.1 文件指纹生成

Vite:

javascript
// 自动生成,无需配置
// 输出:app.a1b2c3d4.js

Webpack:

javascript
// webpack.config.js
output: {
  filename: '[name].[contenthash:8].js'
}
// 输出:app.a1b2c3d4.js

Gulp:

javascript
// 需要额外插件
const rev = require('gulp-rev');
const revReplace = require('gulp-rev-replace');

gulp.task('js', () => {
  return gulp.src('src/js/**/*.js')
    .pipe(uglify())
    .pipe(rev())
    .pipe(gulp.dest('dist/js'))
    .pipe(rev.manifest())
    .pipe(gulp.dest('dist'));
});

3.5.2 缓存策略

现代构建工具的优势:

  1. 内容哈希(Content Hash)

    • 文件内容改变时,哈希值自动更新
    • 未改变的文件保持相同哈希,利用浏览器缓存
  2. 代码分割(Code Splitting)

    • 将第三方库单独打包
    • 业务代码变更不影响库文件缓存
  3. 长期缓存(Long-term Caching)

    • 配合CDN实现最佳缓存策略
    • 自动处理模块依赖关系

4. 最佳实践建议

4.1 缓存策略选择

4.1.1 根据项目阶段选择

开发阶段:

  • 使用 add-version.js 脚本 + 时间戳
  • Nginx配置完全不缓存
  • 快速迭代,及时看到更改

测试阶段:

  • 使用 add-version.js 脚本 + 版本号
  • Nginx配置短时间缓存(5-10分钟)
  • 便于测试缓存相关问题

生产阶段:

  • 优先使用现代构建工具(Vite/Webpack)
  • 配置长期缓存策略
  • 结合CDN优化性能

4.1.2 混合策略

nginx
# 生产环境配置示例
server {
    # 应用文件短缓存
    location ~* ^/js/(app|main)\.[a-f0-9]+\.js$ {
        expires 1h;
        add_header Cache-Control "public, max-age=3600";
    }
    
    # 第三方库长缓存
    location ~* ^/js/vendor\.[a-f0-9]+\.js$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    
    # 无版本号的文件不缓存
    location ~* \.js$ {
        add_header Cache-Control "no-cache, must-revalidate";
    }
}

4.2 监控和调试

4.2.1 缓存状态检查

浏览器开发者工具:

  1. Network面板查看缓存状态
  2. Application面板清除缓存测试
  3. 使用硬刷新(Ctrl+F5)验证

命令行工具:

bash
# 检查响应头
curl -I http://your-domain.com/js/app.js

# 检查缓存状态
curl -H "If-None-Match: \"abc123\"" http://your-domain.com/js/app.js

4.2.2 自动化测试

创建缓存测试脚本 test-cache.js

javascript
const axios = require('axios');

async function testCache(url) {
  try {
    const response = await axios.get(url);
    console.log(`URL: ${url}`);
    console.log(`Cache-Control: ${response.headers['cache-control']}`);
    console.log(`ETag: ${response.headers.etag}`);
    console.log(`Last-Modified: ${response.headers['last-modified']}`);
    console.log('---');
  } catch (error) {
    console.error(`Error testing ${url}:`, error.message);
  }
}

// 测试多个文件
const testUrls = [
  'http://localhost/js/app.js',
  'http://localhost/js/vendor.js',
  'http://localhost/css/style.css'
];

testUrls.forEach(testCache);

4.3 性能优化建议

4.3.1 资源优先级

html
<!-- 关键资源预加载 -->
<link rel="preload" href="/js/app.js?v=123" as="script">
<link rel="preload" href="/css/critical.css?v=123" as="style">

<!-- 非关键资源延迟加载 -->
<script src="/js/analytics.js?v=123" defer></script>
<script src="/js/widgets.js?v=123" async></script>

4.3.2 CDN配置

javascript
// CDN缓存配置示例
const cdnConfig = {
  // 静态资源CDN
  staticCDN: 'https://static.example.com',
  
  // 缓存策略
  cacheRules: {
    // JS文件:1小时缓存
    js: 'max-age=3600, s-maxage=86400',
    
    // CSS文件:1天缓存
    css: 'max-age=86400, s-maxage=604800',
    
    // 图片:1周缓存
    images: 'max-age=604800, s-maxage=2592000'
  }
};

4.4 故障排除

4.4.1 常见问题

问题1:版本号添加后仍然缓存

bash
# 检查Nginx配置是否生效
nginx -t && nginx -s reload

# 检查浏览器缓存
# 使用无痕模式或清除缓存测试

问题2:脚本执行失败

bash
# 检查Node.js版本
node --version

# 检查文件权限
ls -la add-version.js

# 查看详细错误信息
node add-version.js --verbose

问题3:构建工具迁移问题

bash
# 清除node_modules重新安装
rm -rf node_modules package-lock.json
npm install

# 检查配置文件语法
npm run build -- --dry-run

4.4.2 调试技巧

  1. 逐步验证:从简单配置开始,逐步添加复杂功能
  2. 日志记录:在关键步骤添加日志输出
  3. 版本对比:保留工作版本的配置作为参考
  4. 环境隔离:在开发环境充分测试后再部署生产

总结

本文档提供了三种主要的前端缓存解决方案:

  1. Nginx配置:适合快速解决缓存问题,配置简单但不够精细
  2. 版本号脚本:平衡了简单性和有效性,适合传统项目
  3. 现代构建工具:提供最佳的长期解决方案,适合新项目或重构

选择合适的方案需要考虑项目规模、团队技术栈、维护成本等因素。建议从简单方案开始,逐步向现代化工具迁移,以获得更好的开发体验和性能表现。


附录:add-version.js 完整代码

A.1 脚本源码

以下是 add-version.js 的完整源代码,可以直接复制使用:

javascript
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

/**
 * 为HTML文件中的JS文件引用添加版本号
 * 防止浏览器缓存导致更新后无法加载新内容
 */
class VersionAdder {
    constructor(htmlFilePath, customVersion = null) {
        this.htmlFilePath = htmlFilePath;
        this.version = customVersion || Date.now().toString();
        this.isCustomVersion = customVersion !== null;
    }

    /**
     * 读取HTML文件内容
     */
    readHtmlFile() {
        try {
            const content = fs.readFileSync(this.htmlFilePath, 'utf8');
            console.log(`✓ 成功读取文件: ${this.htmlFilePath}`);
            return content;
        } catch (error) {
            console.error(`✗ 读取文件失败: ${error.message}`);
            process.exit(1);
        }
    }

    /**
     * 为JS文件添加版本号
     * @param {string} htmlContent - HTML文件内容
     * @returns {string} - 修改后的HTML内容
     */
    addVersionToJsFiles(htmlContent) {
        let modifiedContent = htmlContent;
        let modifiedCount = 0;

        // 匹配所有script标签的src属性(包括带查询参数的)
        const scriptRegex = /<script\s+[^>]*src=["']([^"']*\.js[^"']*)["'][^>]*>/gi;
        
        modifiedContent = modifiedContent.replace(scriptRegex, (match, src) => {
            // 跳过外部CDN链接
            if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) {
                console.log(`- 跳过外部链接: ${src}`);
                return match;
            }

            let cleanSrc = src;
            let action = '添加版本号';
            
            // 如果已经有版本号,先移除旧版本号
            if (src.includes('?v=') || src.includes('&v=')) {
                // 移除版本号参数,处理各种情况
                cleanSrc = src.replace(/[?&]v=[^&]*/, '').replace(/^&/, '?').replace(/&&/g, '&');
                action = '更新版本号';
            }

            // 添加新版本号
            const versionedSrc = `${cleanSrc}?v=${this.version}`;
            const newMatch = match.replace(src, versionedSrc);
            
            console.log(`✓ ${action}: ${src} -> ${versionedSrc}`);
            modifiedCount++;
            
            return newMatch;
        });

        console.log(`\n总共为 ${modifiedCount} 个JS文件添加了版本号`);
        return modifiedContent;
    }

    /**
     * 保存修改后的HTML文件
     * @param {string} content - 修改后的内容
     */
    saveHtmlFile(content) {
        try {
            // 创建备份文件
            const backupPath = this.htmlFilePath + '.backup';
            fs.copyFileSync(this.htmlFilePath, backupPath);
            console.log(`✓ 创建备份文件: ${backupPath}`);

            // 保存修改后的文件
            fs.writeFileSync(this.htmlFilePath, content, 'utf8');
            console.log(`✓ 保存修改后的文件: ${this.htmlFilePath}`);
        } catch (error) {
            console.error(`✗ 保存文件失败: ${error.message}`);
            process.exit(1);
        }
    }

    /**
     * 执行版本号添加流程
     */
    run() {
        console.log('开始为JS文件添加版本号...');
        if (this.isCustomVersion) {
            console.log(`自定义版本号: ${this.version}\n`);
        } else {
            console.log(`时间戳版本号: ${this.version}\n`);
        }

        // 1. 读取HTML文件
        const htmlContent = this.readHtmlFile();

        // 2. 添加版本号
        const modifiedContent = this.addVersionToJsFiles(htmlContent);

        // 3. 保存文件
        this.saveHtmlFile(modifiedContent);

        console.log('\n✓ 版本号添加完成!');
    }
}

// 主程序入口
function main() {
    const args = process.argv.slice(2);
    
    // 解析命令行参数
    let htmlFilePath = 'index.html';
    let customVersion = null;
    
    // 处理帮助信息
    if (args.includes('-h') || args.includes('--help')) {
        showHelp();
        return;
    }
    
    // 解析参数
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (arg === '-v' || arg === '--version') {
            customVersion = args[i + 1];
            if (!customVersion) {
                console.error('✗ 错误: -v/--version 参数需要指定版本号值');
                showHelp();
                process.exit(1);
            }
            i++; // 跳过版本号值
        } else if (!arg.startsWith('-')) {
            htmlFilePath = arg;
        }
    }
    
    // 如果没有指定HTML文件,使用默认值
    if (!htmlFilePath || htmlFilePath.startsWith('-')) {
        htmlFilePath = 'index.html';
    }
    
    // 如果是相对路径,转换为绝对路径
    if (!path.isAbsolute(htmlFilePath)) {
        htmlFilePath = path.resolve(process.cwd(), htmlFilePath);
    }

    // 检查文件是否存在
    if (!fs.existsSync(htmlFilePath)) {
        console.error(`✗ 文件不存在: ${htmlFilePath}`);
        showHelp();
        process.exit(1);
    }

    // 创建版本添加器并执行
    const versionAdder = new VersionAdder(htmlFilePath, customVersion);
    versionAdder.run();
}

// 显示帮助信息
function showHelp() {
    console.log('为HTML文件中的JS文件引用添加版本号');
    console.log('\n使用方法:');
    console.log('  node add-version.js [选项] [html文件路径]');
    console.log('\n选项:');
    console.log('  -v, --version <版本号>  指定自定义版本号(如果不指定,默认使用时间戳)');
    console.log('  -h, --help             显示帮助信息');
    console.log('\n示例:');
    console.log('  node add-version.js index.html                    # 使用时间戳版本号');
    console.log('  node add-version.js -v 1.2.3 index.html          # 使用自定义版本号 1.2.3');
    console.log('  node add-version.js --version v2.0 ./dist/index.html  # 使用自定义版本号 v2.0');
    console.log('  node add-version.js -v abc123                     # 使用自定义版本号 abc123,默认处理 index.html');
    console.log('\n功能:');
    console.log('  - 为本地JS文件添加版本号参数(?v=版本号)');
    console.log('  - 跳过外部CDN链接和已有版本号的文件');
    console.log('  - 自动创建备份文件(.backup后缀)');
    console.log('  - 提供详细的操作日志');
}

// 如果直接运行此脚本
if (require.main === module) {
    main();
}

module.exports = VersionAdder;

2.1.2 核心功能说明

VersionAdder 类的主要方法:

  1. readHtmlFile() - 安全读取HTML文件,包含完整的错误处理
  2. addVersionToJsFiles() - 核心版本号添加逻辑,使用正则表达式匹配script标签
  3. saveHtmlFile() - 保存文件并自动创建备份
  4. run() - 执行完整的版本号添加流程

智能识别功能:

  • ✅ 自动识别本地JS文件(跳过外部CDN链接)
  • ✅ 处理已有版本号的情况(更新而非重复添加)
  • ✅ 支持各种script标签格式
  • ✅ 详细的日志输出和统计信息

安全机制:

  • ✅ 自动创建备份文件(.backup后缀)
  • ✅ 完整的错误处理和退出机制
  • ✅ 文件存在性检查
  • ✅ 路径处理(相对路径转绝对路径)

2.1.3 使用示例

处理前的HTML文件:

html
<!DOCTYPE html>
<html>
<head>
    <title>示例页面</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>Hello World</h1>
    <script src="js/main.js"></script>
    <script src="js/utils.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script src="js/old.js?v=123"></script>
</body>
</html>

执行命令:

bash
node add-version.js -v v1.2.3 index.html

处理后的HTML文件:

html
<!DOCTYPE html>
<html>
<head>
    <title>示例页面</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>Hello World</h1>
    <script src="js/main.js?v=v1.2.3"></script>
    <script src="js/utils.js?v=v1.2.3"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script src="js/old.js?v=v1.2.3"></script>
</body>
</html>

控制台输出:

开始为JS文件添加版本号...
自定义版本号: v1.2.3

✓ 成功读取文件: /path/to/index.html
✓ 添加版本号: js/main.js -> js/main.js?v=v1.2.3
✓ 添加版本号: js/utils.js -> js/utils.js?v=v1.2.3
- 跳过外部链接: https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js
✓ 更新版本号: js/old.js?v=123 -> js/old.js?v=v1.2.3
✓ 创建备份文件: /path/to/index.html.backup
✓ 保存修改后的文件: /path/to/index.html

总共为 3 个JS文件添加了版本号

✓ 版本号添加完成!

2.1.4 模块化使用

除了命令行使用,还可以作为Node.js模块导入使用:

javascript
// 导入模块
const VersionAdder = require('./add-version.js');

// 创建实例
const versionAdder = new VersionAdder('index.html', 'v1.2.3');

// 执行版本号添加
versionAdder.run();

// 或者分步执行
const htmlContent = versionAdder.readHtmlFile();
const modifiedContent = versionAdder.addVersionToJsFiles(htmlContent);
versionAdder.saveHtmlFile(modifiedContent);

在构建脚本中使用:

javascript
// build.js
const VersionAdder = require('./add-version.js');
const { execSync } = require('child_process');

async function build() {
    try {
        // 1. 执行构建
        console.log('开始构建...');
        execSync('npm run build', { stdio: 'inherit' });
        
        // 2. 添加版本号
        console.log('添加版本号...');
        const version = process.env.BUILD_VERSION || Date.now().toString();
        const versionAdder = new VersionAdder('./dist/index.html', version);
        versionAdder.run();
        
        console.log('构建完成!');
    } catch (error) {
        console.error('构建失败:', error.message);
        process.exit(1);
    }
}

build();

2.2 使用方法

bash
# 基本用法 - 使用时间戳版本号
node add-version.js index.html

# 使用自定义版本号
node add-version.js -v 1.2.3 index.html

# 处理其他HTML文件
node add-version.js -v v2.0 ./dist/index.html

2.3 脚本功能特性

  • ✅ 自动识别本地JS文件
  • ✅ 跳过外部CDN链接
  • ✅ 支持更新已有版本号
  • ✅ 自动创建备份文件
  • ✅ 详细的操作日志
  • ✅ 支持自定义版本号

2.4 集成到部署流程

2.4.1 手动部署脚本

bash
#!/bin/bash
# deploy.sh

echo "开始部署流程..."

# 1. 构建项目(如果有构建步骤)
# npm run build

# 2. 为JS文件添加版本号
echo "为JS文件添加版本号..."
node add-version.js -v $(date +%Y%m%d%H%M%S) index.html

# 3. 上传到服务器
echo "上传文件到服务器..."
# rsync -avz ./dist/ user@server:/var/www/html/

echo "部署完成!"

2.4.2 自动化部署脚本

javascript
// auto-deploy.js
const { execSync } = require('child_process');
const VersionAdder = require('./add-version.js');

class AutoDeployer {
    constructor(htmlFile, version) {
        this.htmlFile = htmlFile;
        this.version = version || Date.now().toString();
    }

    async deploy() {
        try {
            console.log('🚀 开始自动化部署...');
            
            // 1. 添加版本号
            console.log('📝 添加版本号...');
            const versionAdder = new VersionAdder(this.htmlFile, this.version);
            versionAdder.run();
            
            // 2. 执行构建(如果需要)
            console.log('🔨 执行构建...');
            // execSync('npm run build', { stdio: 'inherit' });
            
            // 3. 上传文件(根据实际情况修改)
            console.log('📤 上传文件...');
            // execSync('rsync -avz ./dist/ user@server:/var/www/html/', { stdio: 'inherit' });
            
            console.log('✅ 部署完成!');
        } catch (error) {
            console.error('❌ 部署失败:', error.message);
            process.exit(1);
        }
    }
}

// 使用示例
const deployer = new AutoDeployer('index.html', 'v1.0.0');
deployer.deploy();

2.5 优缺点

优点:

  • 保持缓存优势,只有更新时才重新下载
  • 不影响现有代码结构
  • 可以精确控制版本号
  • 适合渐进式升级

缺点:

  • 需要手动或半自动执行
  • 需要修改部署流程
  • 版本号管理需要额外考虑

方案三:升级构建工具

3.1 升级到Gulp

3.1.1 安装Gulp

bash
# 全局安装gulp-cli
npm install -g gulp-cli

# 项目安装gulp
npm install --save-dev gulp gulp-uglify gulp-clean-css gulp-rename gulp-rev

3.1.2 Gulpfile.js配置

javascript
// gulpfile.js
const gulp = require('gulp');
const uglify = require('gulp-uglify');
const cleanCSS = require('gulp-clean-css');
const rev = require('gulp-rev');
const revReplace = require('gulp-rev-replace');
const del = require('del');

// 清理输出目录
function clean() {
    return del(['dist/**/*']);
}

// 处理JS文件
function scripts() {
    return gulp.src('src/js/**/*.js')
        .pipe(uglify())
        .pipe(rev())
        .pipe(gulp.dest('dist/js'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('dist'));
}

// 处理CSS文件
function styles() {
    return gulp.src('src/css/**/*.css')
        .pipe(cleanCSS())
        .pipe(rev())
        .pipe(gulp.dest('dist/css'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('dist'));
}

// 处理HTML文件,替换资源引用
function html() {
    const manifest = gulp.src('dist/rev-manifest.json');
    
    return gulp.src('src/*.html')
        .pipe(revReplace({ manifest: manifest }))
        .pipe(gulp.dest('dist'));
}

// 复制其他文件
function copy() {
    return gulp.src(['src/**/*', '!src/js/**/*', '!src/css/**/*', '!src/*.html'])
        .pipe(gulp.dest('dist'));
}

// 构建任务
const build = gulp.series(clean, gulp.parallel(scripts, styles), html, copy);

// 监听文件变化
function watch() {
    gulp.watch('src/js/**/*.js', scripts);
    gulp.watch('src/css/**/*.css', styles);
    gulp.watch('src/*.html', html);
}

exports.clean = clean;
exports.scripts = scripts;
exports.styles = styles;
exports.html = html;
exports.copy = copy;
exports.build = build;
exports.watch = watch;
exports.default = build;

3.1.3 package.json脚本

json
{
  "scripts": {
    "build": "gulp build",
    "dev": "gulp watch",
    "clean": "gulp clean"
  }
}

3.2 升级到Webpack

3.2.1 安装Webpack

bash
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin mini-css-extract-plugin
npm install --save-dev css-loader style-loader file-loader

3.2.2 webpack.config.js配置

javascript
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'js/[name].[contenthash:8].js',
        clean: true,
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[name].[contenthash:8][ext]'
                }
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            filename: 'index.html',
        }),
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash:8].css',
        }),
    ],
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
            },
        },
    },
};

3.3 升级到Vite(推荐)

3.3.1 安装Vite

bash
npm install --save-dev vite

3.3.2 vite.config.js配置

javascript
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        outDir: 'dist',
        assetsDir: 'assets',
        rollupOptions: {
            output: {
                // 自定义chunk文件名
                chunkFileNames: 'js/[name]-[hash].js',
                entryFileNames: 'js/[name]-[hash].js',
                assetFileNames: (assetInfo) => {
                    const info = assetInfo.name.split('.');
                    const ext = info[info.length - 1];
                    if (/\.(css)$/.test(assetInfo.name)) {
                        return `css/[name]-[hash].${ext}`;
                    }
                    if (/\.(png|jpe?g|gif|svg)$/.test(assetInfo.name)) {
                        return `images/[name]-[hash].${ext}`;
                    }
                    return `assets/[name]-[hash].${ext}`;
                },
            },
        },
    },
    server: {
        port: 3000,
        open: true,
    },
});

3.4 优缺点

优点:

  • 现代化的构建流程
  • 自动处理文件版本号
  • 支持代码分割和优化
  • 更好的开发体验
  • 长期维护成本低

缺点:

  • 需要重构项目结构
  • 学习成本较高
  • 可能影响现有功能

方案选择建议

4.1 紧急情况

如果项目急需解决缓存问题,建议使用方案一(Nginx配置),可以立即生效。

4.2 短期解决

如果需要保持性能的同时解决缓存问题,建议使用方案二(动态版本号),配合自动化脚本。

4.3 长期规划

如果项目有长期维护计划,强烈建议使用方案三(升级构建工具),特别是升级到Vite。

最佳实践

5.1 缓存策略

nginx
# 推荐的Nginx缓存配置
location ~* \.(js|css)$ {
    # 设置较长的缓存时间,但使用版本号控制
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Vary "Accept-Encoding";
}

location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    # 静态资源长期缓存
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.html$ {
    # HTML文件不缓存
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

5.2 版本号管理

javascript
// 版本号生成策略
const version = process.env.NODE_ENV === 'production' 
    ? require('./package.json').version 
    : Date.now().toString();

// 在构建时使用
const versionAdder = new VersionAdder('index.html', version);

5.3 自动化部署

yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm install
      - name: Add version to JS files
        run: node add-version.js -v ${{ github.sha }} index.html
      - name: Deploy to server
        run: |
          # 部署命令

总结

前端老项目的缓存优化是一个系统工程,需要根据项目实际情况选择合适的方案。建议:

  1. 立即解决:使用Nginx配置
  2. 短期优化:使用动态版本号
  3. 长期规划:升级到现代构建工具

无论选择哪种方案,都要确保:

  • 不影响现有功能
  • 保持或提升性能
  • 便于后续维护
  • 有完整的测试验证

通过合理的缓存策略,可以显著提升用户体验,减少服务器负载,提高应用性能。