前两天,一位 jy [长路漫漫且灿灿] (名怪好听的😁)告诉我说引入 Tailwind 出错:
作为一个勤快的博主,怎么能不极速解决观众老爷的烦恼呢?然后早上6点就爬起来,看看问题究竟在哪 ~
1、怎么欢(fan)车了!
说在前面:这里需要声明一下,之所以遇到这个坑是因为,我基于“从0到1搭建React18+TS4.x+Webpack5项目” 中引入 Tailwind。如果你正常使用官方脚手架 —— CRA 来搭建项目,并且按照 Tailwind 官方给出的教程来安装使用,是不会遇到这个问题,大家可以不用担心。
据 jy 说,我按照官方给出的 installation/using-postcss 教程,安装并使用:
预期应该是这样的:
但实际是这样的,并没有任何效果
果然如之前评论区 jy 所说的(见上图),出问题了!
2、问题出在哪?
我首先对项目进行了 build,然后 serve -S dist 在浏览器中查看了一下 build产物:
我发现 Tailwind 的 class name 被 Module CSS 重新命令了,导致元素上的 class name 在style标中压根找不到对应的 css rules 那这应该就是问题的根源所在了。
3、怎么解决的?
从上面的分析,基本清楚了问题点关键点在于编译后的 class name 被重新命名了,那就是模块化的问题。所以第一时间想到了 css-loader(因为它就是干这事儿的),在 webpack 官方文档上找到了对应的资料:传送门。
css-loader 中的 "modules" 选项默认启用此行为。当你将 "modules" 设置为 "true" 时,它将自动生成每个组件的唯一类名,并用这些标识符替换 CSS 中的类名。但是,你也可以通过设置 "mode" 属性来自定义此行为。
modules 中的 "mode" 属性有四个可能的值:"local"、"global"、"pure" 和 "icss"。以下是每个值的含义:
local:默认值。CSS 模块会使用本地作用域策略进行处理。这意味着每个类名都会被转换为一个唯一的名称,该名称作用域限定于该模块。当希望避免不同组件之间的命名冲突时,这种模式对于构建应用程序非常有用。在此模式下,生成的类名仅在定义它们的组件中具有本地作用域。例如,如果你有一个名为 "Button" 的组件并在其 CSS 中定义了一个类名 "primary",则生成的类名可能类似于 "Button__primary__3y78d"。这样,"primary" 类名仅适用于 "Button" 组件,不会影响具有 "primary" 类名的任何其他组件。icss:这代表 "Interoperable CSS",CSS 模块会使用Interoperable CSS (ICSS)规范进行处理。在此模式下,使用 ICSS 标准加载 CSS 模块,这允许更高级的功能,例如可组合的 CSS 和动态主题。这意味着模块导出一个普通对象,其中包含从本地类名到全局类名的映射,可以用于将样式导入到其他模块中。这种模式对于构建可由其他应用程序消费的库非常有用。global:在此模式下,CSS 类名是全局的,可以在整个应用程序中使用。这对于定义在多个组件中使用的实用程序类很有用。"global" 模式生成的类名不限于特定组件的范围。这对于定义可以在不同组件之间重复使用的实用类非常有用。例如,如果你有一个名为"text-center"的类,用于水平居中文本,可以定义它一次,然后在不同组件中使用它,而无需每次重新定义它。pure:"pure"模式类似于"local"模式,但它还从最终输出中删除未使用的 CSS 类名。这可以帮助减小 CSS 文件的大小并提高性能。
而且我发现在 CRA 创建的 react 项目中,也对 pure css 和 module css 做了细节的处理
于是,我将 webpack.base.ts 里面关于样式的 options 修改了一下:
并引入了 Tailwind 必要的配置:
完整的 webpack.base.ts 代码:
import { Configuration, DefinePlugin } from 'webpack' import HtmlWebpackPlugin from 'html-webpack-plugin' import WebpackBar from 'webpackbar' import * as dotenv from 'dotenv' import { isDev } from './constants'但 const path = require('path') const MiniCssExtractPlugin = require('mini-css-extract-plugin') console.log('NODE_ENV', process.env.NODE_ENV) console.log('BASE_ENV', process.env.BASE_ENV) // 加载配置文件 const envConfig = dotenv.config({ path: path.resolve(__dirname, `../env/.env.${process.env.BASE_ENV}`) }) const tsxRegex = /\.(ts|tsx)$/ const cssRegex = /\.css$/ const sassRegex = /\.(scss|sass)$/ const lessRegex = /\.less$/ const stylRegex = /\.styl$/ const imageRegex = /\.(png|jpe?g|gif|svg)$/i const fontRegex = /\.(ttf|woff2?|eot|otf)$/ const mediaRegex = /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/ const jsonRegex = /\.json$/ const getStyleLoaders = (cssLoaderOpts: any) => { const loaders = [ isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css { /** 三个作用: * 1. CSS 模块化:将 CSS 模块化可以避免命名冲突,提高代码复用性。 * 2. 自动添加浏览器前缀:在 CSS 样式中自动添加浏览器前缀,以提高浏览器兼容性。 * 3. 将 CSS 中的 URL 转换成 require:将 CSS 中的图片路径转换成 Webpack 所需的 require 路径。 */ loader: 'css-loader', options: cssLoaderOpts }, 'postcss-loader' ] return loaders } const baseConfig: Configuration = { entry: path.join(__dirname, '../src/index.tsx'), // 入口文件 // 打包出口文件 output: { filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称 path: path.join(__dirname, '../dist'), // 打包结果输出路径 clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了 publicPath: '/', // 打包后文件的公共前缀路径 assetModuleFilename: 'images/[name].[contenthash:8][ext]' }, // loader 配置 module: { rules: [ { test: tsxRegex, // 匹配.ts, tsx文件 exclude: /node_modules/, use: 'babel-loader' // use: ['thread-loader', 'babel-loader'] // 项目变大之后再开启多进程loader }, { test: cssRegex, // 匹配 css 文件 use: getStyleLoaders({ // importLoaders: 1, // 指定在 CSS 中 @import 的文件也要被 css-loader 处理,默认为 0。 // 启用 CSS 模块化,默认为 false。 modules: { mode: 'icss', localIdentName: '[path][name]__[local]--[hash:5]' } }) }, { test: lessRegex, use: [ ...getStyleLoaders({ importLoaders: 2, // 指定在 CSS 中 @import 的文件也要被 css-loader 处理,默认为 0。 // 启用 CSS 模块化,默认为 false。 modules: { mode: 'local', localIdentName: '[path][name]__[local]--[hash:5]' } }), { loader: 'less-loader', options: { lessOptions: { importLoaders: 2, // 可以加入modules: true,这样就不需要在less文件名加module了 modules: true, // 如果要在less中写类型js的语法,需要加这一个配置 javascriptEnabled: true } } } ] }, { test: sassRegex, use: [ ...getStyleLoaders({ importLoaders: 2, // 指定在 CSS 中 @import 的文件也要被 css-loader 处理,默认为 0。 // 启用 CSS 模块化,默认为 false。 modules: { mode: 'local', localIdentName: '[path][name]__[local]--[hash:5]' } }), { loader: 'sass-loader', options: { implementation: require('sass') // 使用dart-sass代替node-sass } } ] }, { test: stylRegex, use: [ ...getStyleLoaders({ importLoaders: 2, // 指定在 CSS 中 @import 的文件也要被 css-loader 处理,默认为 0。 // 启用 CSS 模块化,默认为 false。 modules: { mode: 'local', localIdentName: '[path][name]__[local]--[hash:5]' } }), 'stylus-loader' ] }, { test: imageRegex, // 匹配图片文件 type: 'asset', // 设置资源处理的类型为asset parser: { // 转为inline dataUrl的条件 dataUrlCondition: { // 默认限制为8kb,现在调整限制为10kb,大文件直接作为asset/resource类型文件输出 maxSize: 10 * 1024 } }, generator: { filename: 'static/images/[name].[contenthash:8][ext]' // 文件输出目录和命名 } }, { // 匹配json文件 test: jsonRegex, type: 'asset/resource', // 将json文件视为文件类型 generator: { // 这里专门针对json文件的处理 filename: 'static/fonts/[name].[contenthash:8][ext]' } }, { test: fontRegex, // 匹配字体图标文件 type: 'asset/resource', // type选择asset // parser: { // dataUrlCondition: { // maxSize: 10 * 1024, // 小于10kb转base64 // } // }, generator: { filename: 'static/json/[name].[contenthash:8][ext]' // 文件输出目录和命名 } }, { test: mediaRegex, // 匹配媒体文件 type: 'asset', // type选择asset parser: { dataUrlCondition: { maxSize: 10 * 1024 // 小于10kb转base64 } }, generator: { filename: 'static/media/[name].[contenthash:8][ext]' // 文件输出目录和命名 } } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.less', '.css', '.scss', '.sass', '.styl', '.json'], // 别名需要配置两个地方,这里和 tsconfig.json alias: { '@': path.join(__dirname, '../src') } // modules: [path.join(__dirname, "../node_modules")], // 查找第三方模块只在本项目的node_modules中查找 }, // plugins 的配置 plugins: [ new HtmlWebpackPlugin({ title: 'webpack5-react-ts', filename: 'index.html', // 复制 'index.html' 文件,并自动引入打包输出的所有资源(js/css) template: path.join(__dirname, '../public/index.html'), inject: true, // 自动注入静态资源 hash: true, cache: false, // 压缩html资源 minify: { removeAttributeQuotes: true, collapseWhitespace: true, // 去空格 removeComments: true, // 去注释 minifyJS: true, // 在脚本元素和事件属性中缩小JavaScript(使用UglifyJS) minifyCSS: true // 缩小CSS样式元素和样式属性 }, nodeModules: path.resolve(__dirname, '../node_modules') }), new DefinePlugin({ 'process.env': JSON.stringify(envConfig.parsed), 'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) }), new WebpackBar({ color: '#85d', // 默认green,进度条颜色支持HEX basic: false, // 默认true,启用一个简单的日志报告器 profile: false // 默认false,启用探查器。 }) ], cache: { /* webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,通过配置 webpack 持久化缓存,来缓存生成的 webpack 模块和 chunk, 改善下一次打包的构建速度,可提速 90% 左右,配置也简单 */ type: 'filesystem' // 使用文件缓存 } } export default baseConfig
具体的可看我的源代码:源码
然后在 App.tsx 中加入个用 Tailwind.css 写的标签:
重启项目,便看到效果了:
build 后的 className 也没有被重命名为带模块前缀的样子:
在开发的时候,如果你不希望每次引入 Tailwind 样式之后,都要重新run一次,可以使用官方提供的这种方式:
在控制台执行,它会监听你文件的变化,然后自动更新样式表文件:
npx tailwindcss -i ./src/tailwind.css -c ./tailwind.config.js -o ./src/index.css --watch
你可以使用 --watch 或 --w 标志来启动一个观察进程,并在你做任何修改时自动重建你的CSS。
不过这种建监听的方式有点 stupid someway,后续再看看有啥更好办法吧 ~ 如果掘友们有好的 idea,欢迎评论区留言讨论。
然后就可以在项目中愉快地使用 Tailwind 了~













