如何对雪碧图路径在不同 CDN 域名下做替换

解读

雪碧图(CSS Sprite)在上线时往往要替换成 CDN 地址,以便利用多域名并行下载边缘缓存回源策略。国内项目常见三种场景:

  1. 本地开发用相对路径 /assets/sprite.png
  2. 测试环境用内部 CDN https://static-test.xxx.com
  3. 正式环境用外部 CDN https://static.xxx.comhttps://img{1-4}.xxx.com

Grunt 作为配置驱动的任务运行器,需要在构建阶段就把路径写死到 CSS 里,而不是在服务器端做二次替换。因此面试题考察的是:

  • 如何一次性生成多套 CSS(或一份 CSS 多次替换)以对应不同 CDN
  • 如何保证雪碧图名带 hash 时路径仍能正确替换
  • 如何与** spritesmith 等雪碧图生成插件**协同
  • 如何避免手动改配置,让 CI 直接 grunt build:prod 即可发布

知识点

  1. Grunt 多阶段构建机制grunt.initConfig 里可以使用 <%= %> 模板语法,也可通过 grunt.config.set 在运行时动态改值
  2. 文件指纹(hash):国内上线要求强缓存 + 文件指纹,雪碧图生成后文件名会带 sprite.9d8e3f.png,因此正则替换必须匹配 hash 部分
  3. spritesmith 输出grunt-spritesmith 会同时生成 .png.scss(或 .less),其中变量 $sprite-url 默认是 sprite.png,需要被覆盖
  4. 字符串替换插件grunt-string-replacegrunt-replace 支持基于函数的替换,可动态读取 CDN 域名
  5. 环境变量注入:国内公司普遍在 Jenkins、GitLab CI 里配置 export CDN_DOMAIN=//img1.xxx.com,Grunt 通过 process.env.CDN_DOMAIN 读取
  6. 并发域名优化:为了突破浏览器单域名 6 连接限制,正式环境往往把图片拆到 img1~img4,需要按文件名 hash 取模做域名分片
  7. 缓存刷新策略:替换后的 CDN 路径必须带版本号或 hash,否则需要手动刷新 CDN,面试时要提到**“文件名即版本”**原则

答案

  1. 安装依赖
npm i -D grunt-spritesmith grunt-string-replace load-grunt-tasks
  1. 目录约定
src/slice/          小图标
src/scss/_sprite.scss  由 spritesmith 生成,不提交
dist/img/           雪碧图输出
dist/css/           最终 css
  1. Gruntfile.js 关键配置
module.exports = function(grunt) {
  require('load-grunt-tasks')(grunt);

  // 1. 根据环境拿到 CDN 域名
  const env = grunt.option('env') || 'dev';
  const CDN_MAP = {
    dev: '',
    test: '//static-test.xxx.com',
    prod: '//img{idx}.xxx.com'   // {idx} 会被替换成 1~4
  };
  const cdnBase = CDN_MAP[env];

  // 2. 生成雪碧图
  grunt.initConfig({
    sprite: {
      all: {
        src: 'src/slice/*.png',
        destImg: 'dist/img/sprite.png',
        destCSS: 'src/scss/_sprite.scss',
        cssFormat: 'scss',
        // 关键:让生成的 scss 里 url 保持可替换标记
        cssOpts: { functions: false } // 禁止内置函数,方便后面统一替换
      }
    },

    // 3. 编译 scss
    sass: {
      options: { implementation: require('sass') },
      dist: { files: { 'dist/css/main.css': 'src/scss/main.scss' } }
    },

    // 4. 字符串替换:把本地路径换成 CDN
    'string-replace': {
      dist: {
        files: [{ expand: true, cwd: 'dist/css', src: '*.css', dest: 'dist/css' }],
        options: {
          replacements: [{
            pattern: /url\(\s*['"]?([^'")]+)sprite\.([a-f0-9]+\.)?png['"]?\s*\)/gi,
            replacement: function(match, prefix, hash) {
              // 按文件名 hash 分片,避免全站图片集中到同一域名
              const h = hash ? hash.slice(0, -1) : '';
              const idx = (parseInt(h.slice(-1), 16) % 4) + 1;
              const domain = cdnBase.replace('{idx}', idx);
              return `url(${domain}/img/sprite.${h ? h + '.' : ''}png)`;
            }
          }]
        }
      }
    }
  });

  // 5. 注册任务
  grunt.registerTask('default', ['sprite', 'sass', 'string-replace']);
};
  1. 使用方式
# 本地开发
grunt
# 测试环境
grunt --env=test
# 正式环境
grunt --env=prod
  1. 验证 构建后打开 dist/css/main.css,应出现
.icon-user{background:url(//img3.xxx.com/img/sprite.9d8e3f.png) -0 -0 no-repeat}

且不同图片分散到 img1~img4,满足并行下载强缓存要求

拓展思考

  1. 雪碧图与 HTTP2 的权衡:国内部分大厂已全站 HTTP2,多域名分片反而降低性能,此时可直接用单域名 + 雪碧图,面试可提及“是否还需要雪碧图”需按业务实测
  2. WebP 自适应:国内 CDN 普遍支持自动 WebP,可在替换函数里把 .png 改成 .png?format=webp,并配合 picture 标签做降级
  3. 雪碧图增量更新:当图标只改一个时,整图 hash 变化导致所有图片缓存失效,可引入**“分帧雪碧图”“SVG symbol”**方案,Grunt 侧需拆分任务
  4. 与 Vite/Webpack 共存:老项目仍用 Grunt,新模块用 Vite,可通过**“构建产物约定”让 Grunt 只做雪碧图与路径替换,其余交给 Vite,实现渐进迁移**
  5. 灰度发布:国内常用**“按用户百分比灰度”,可在替换函数里读取环境变量 GRUNT_GRAY_UID,把 5% 用户指向 //img-gray.xxx.com,实现秒级回滚**