如何让PurifyCSSPlugin首次构建时正确识别TSX/JSX中的样式?
我之前也碰到过几乎一模一样的问题,核心原因其实很清楚:首次构建时,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

