You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让PurifyCSSPlugin首次构建时正确识别TSX/JSX中的样式?

解决PurifyCSSPlugin配合CSS Modules首次构建丢失样式的问题

我之前也碰到过几乎一模一样的问题,核心原因其实很清楚:首次构建时,env.outputDirectory里的bundle.js还没生成,PurifyCSS只能从你的TSX源文件里找样式引用,但CSS Modules会把原始类名转换成哈希字符串,源TSX里的title变量和最终CSS里的哈希类名根本匹配不上,所以PurifyCSS误判这些样式未被使用,就给删掉了。而二次构建时,上一次生成的bundle.js已经存在,里面有编译后的哈希类名,PurifyCSS能正确识别,所以效果正常。

下面给你几个可行的解决方案,按推荐程度排序:

1. 改用PurgeCSS Webpack插件(最推荐)

PurifyCSS已经停止维护了,而PurgeCSS是它的继任者,对CSS Modules的支持要友好得多——它可以直接分析webpack的编译模块依赖,不需要依赖磁盘上的文件,首次构建就能正确识别样式引用。

配置步骤:

首先安装依赖:

npm install purgecss-webpack-plugin @fullhuman/postcss-purgecss --save-dev

然后修改webpack配置:

const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');

// ... 其他配置

plugins: [
  // ... 你的MiniCssExtractPlugin等插件
  new PurgeCSSPlugin({
    // 扫描所有TSX源文件
    paths: glob.sync(path.join(__dirname, './**/*.tsx'), { nodir: true }),
    // 自定义提取器,适配CSS Modules的哈希类名
    defaultExtractor: content => {
      // 提取所有可能的类名、ID和伪类,去掉冒号前缀(比如:hover里的hover)
      const matches = content.match(/[\w-/:]+(?<!:)/g) || [];
      // 处理类名:如果是.开头的,去掉点号,因为CSS里的类名不带点
      return matches.map(match => match.startsWith('.') ? match.slice(1) : match);
    },
    // 如果有动态生成的类名,可以在这里添加白名单,比如匹配哈希前缀
    safelist: {
      standard: [],
      // 比如你的CSS Modules哈希前缀是title-,可以加/^title-/
      greedy: [/^title-/]
    }
  })
]

这个方案不需要依赖磁盘文件,PurgeCSS会直接从webpack的编译过程中获取模块内容,所以首次构建就能精准识别被使用的样式,效果和你二次构建的情况一致。

2. 调整构建顺序,先编译TSX到JS再执行PurifyCSS

如果你坚持要用PurifyCSSPlugin,可以分两步构建:先把TSX编译成JS文件(输出到临时目录),再让PurifyCSS扫描这个临时JS文件和源TSX文件,这样首次构建时就能拿到编译后的哈希类名了。

配置示例:

把webpack配置改成数组形式,分两个阶段:

const path = require('path');
const glob = require('glob');
const PurifyCSSPlugin = require('purifycss-webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = [
  // 第一步:仅编译TSX到临时目录,生成JS文件
  {
    entry: './src/index.tsx',
    output: {
      path: path.resolve(__dirname, './temp-build'),
      filename: 'bundle.js'
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/
        }
      ]
    },
    resolve: {
      extensions: ['.tsx', '.ts', '.js']
    }
  },
  // 第二步:处理CSS并执行PurifyCSS,扫描临时JS文件
  {
    entry: './src/index.tsx',
    output: {
      path: env.outputDirectory,
      filename: 'bundle.js'
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/
        },
        {
          test: /\.scss$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: 'css-loader',
              options: { modules: true }
            },
            'sass-loader'
          ]
        }
      ]
    },
    plugins: [
      new MiniCssExtractPlugin({ filename: '[name].css' }),
      new PurifyCSSPlugin({
        paths: glob.sync([
          path.join(__dirname, './**/*.tsx'),
          path.join(__dirname, './temp-build/bundle.js') // 指向临时JS文件
        ]),
        purifyOptions: { info: true, minify: false }
      })
    ],
    resolve: {
      extensions: ['.tsx', '.ts', '.js']
    }
  }
];

运行webpack时会自动按顺序执行两个配置,首次构建就能让PurifyCSS拿到编译后的类名,达到二次构建的效果。

3. 自定义webpack钩子,提前写入JS文件到磁盘

还有一个更hack的方法:通过webpack的emit钩子,在PurifyCSS执行之前,把编译后的JS文件提前写入磁盘,这样glob就能找到它。

配置示例:

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

// ... 其他配置

plugins: [
  // 自定义插件:提前写入JS文件到磁盘
  {
    apply(compiler) {
      compiler.hooks.emit.tapAsync('PreWriteJSAssets', (compilation, callback) => {
        // 遍历所有JS资产,写入到输出目录
        for (const filename in compilation.assets) {
          if (filename.endsWith('.js')) {
            const outputPath = path.join(env.outputDirectory, filename);
            fs.outputFileSync(outputPath, compilation.assets[filename].source());
          }
        }
        callback();
      });
    }
  },
  // 你的PurifyCSSPlugin配置不变
  new PurifyCSSPlugin({
    paths: glob.sync([
      path.join(env.outputDirectory, './*.html'),
      path.join(env.outputDirectory, './**/*.js'),
      path.join(__dirname, './**/*.tsx')
    ]),
    purifyOptions: { info: true, minify: false }
  })
]

这个方法不需要修改整体构建流程,但需要注意清理临时文件,避免影响后续构建。


内容的提问来源于stack exchange,提问作者RasPelikan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:08:03