描述在 grunt 中实现 PurgeCSS 与 Tailwind 整合的步骤

解读

面试官抛出此题,核心想验证三件事:

  1. 你是否真的在生产环境用过 Grunt,而不是只会“npm run”。
  2. 你是否理解 Tailwind “全量生成 → PurgeCSS 按需瘦身” 这一经典性能模型。
  3. 你能否把“Grunt 的插件编排思维”与“PostCSS 生态”无缝衔接,给出可落地、可维护、可CI 的完整方案。
    回答时切忌只罗列插件名,而要体现“任务链顺序、文件流走向、缓存与增量、国内镜像源、多人协作规范”这些工程细节。

知识点

  • grunt-postcsspostcss-purgecss 的调用关系
  • Tailwind 的 JIT 模式 与 PurgeCSS 的取舍(国内 3G 场景下仍推荐 PurgeCSS)
  • Gruntfile.js 中文件对象扩展符(expand:true, cwd/src/dest/flattens)
  • grunt-contrib-watch 的 spawn:false 选项 与 livereload 的端口复用
  • npm 国内镜像(淘宝源、华为源)对 grunt 插件安装速度的影响
  • CI 场景下 如何用 grunt-newer 做增量 Purge,避免 10k+ 类名每次都全量扫描
  • sourcemap 的 inline 与外部文件模式 对线上报错定位的权衡

答案

要在 Grunt 里把 Tailwind 与 PurgeCSS 串成一条可重复、可缓存、可发布的任务链,我按以下 6 步 实施:

  1. 依赖锁定
    在项目根执行
    npm i -D grunt grunt-cli grunt-postcss postcss postcss-cli tailwindcss postcss-purgecss grunt-contrib-watch grunt-newer
    把 npm 源切到淘宝镜像,保证内网 CI 也能 30s 内装完。

  2. 初始化 Tailwind
    npx tailwindcss init 生成 tailwind.config.js;
    在 content 字段里 显式声明所有可能含类名的模板路径(如 src/**/*.{vue,jsx,ts,html,ejs}),避免 PurgeCSS 误杀。

  3. 编写 Gruntfile.js
    核心任务顺序:
    a. postcss:compile —— 先用 tailwindcss 插件把入口 CSS(如 src/styles/index.css)生成完整原子类;
    b. postcss:purge —— 再链式调用 postcss-purgecss,读取上一步产物,按 content 清单做裁剪;
    c. cssmin —— 最后 grunt-contrib-cssmin 压缩,产出 dist/styles/index.[hash].css。
    代码片段(已脱敏):

    grunt.initConfig({
      postcss: {
        compile: {
          options: {
            processors: [require('tailwindcss')('./tailwind.config.js')]
          },
          src: 'src/styles/index.css',
          dest: '.tmp/tailwind.full.css'
        },
        purge: {
          options: {
            processors: [
              require('@fullhuman/postcss-purgecss')({
                content: require('./tailwind.config.js').content,
                defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
              })
            ]
          },
          src: '.tmp/tailwind.full.css',
          dest: 'dist/styles/index.css'
        }
      },
      cssmin: {
        target: {
          files: [{
            expand: true,
            cwd: 'dist/styles',
            src: '*.css',
            dest: 'dist/styles',
            ext: '.min.css'
          }]
        }
      },
      watch: {
        css: {
          files: ['src/styles/**/*.css', 'src/**/*.{html,js,vue}'],
          tasks: ['newer:postcss:compile', 'newer:postcss:purge', 'cssmin'],
          options: { spawn: false, livereload: 35729 }
        }
      }
    });
    grunt.registerTask('default', ['postcss:compile', 'postcss:purge', 'cssmin']);
    
  4. 增量与缓存
    使用 grunt-newer 让 watch 阶段只重编被改动的文件;
    在 CI 里再配一层 cache-loader 把 .tmp 目录缓存到 GitLab Runner 的 /cache 分区,平均节省 40% 构建时长。

  5. 多人协作规范
    把 tailwind.config.js 与 Gruntfile.js 纳入 eslint + prettier 强制格式化;
    在 README 中约定“新增模板文件必须同步到 tailwind.content,否则 Purge 后样式丢失由开发者自行背锅”,并在 MR 里用 danger-js 自动检测。

  6. 线上验证
    发版后通过 Chrome Coverage 验证 CSS 体积下降 85%+;
    开 Sentry sourcemap 可快速回溯到原始 Tailwind 类名,方便线上事故定位。

拓展思考

  • 如果项目切到 Vite,是否还需要 PurgeCSS?
    Tailwind 3 的 JIT 已内置按需生成,但 国内仍有老项目卡在 Grunt + Tailwind 2,此时 PurgeCSS 仍是刚需;掌握本方案可平滑迁移,无需一次性推翻构建链。

  • 如何进一步压缩?
    可在 cssmin 之后加 grunt-assets-inline 把 <1KB 的 min.css 直接内联到 HTML,减少一次 HTTPS 往返,对弱网用户尤其明显。

  • 如何防止误杀动态类名?
    在 purgecss 选项里加 safelist: [/^data-v-/, /el-table/, 'dark'] 正则,把 Vue scoped、Element-Plus 与业务手动切换的 dark 模式白名单化,避免线上样式“消失术”。