【React】记一次在 React18+TS4.x+Webpack5 项目中引入 Tailwind 的 “坑” ~

1,537 阅读9分钟

本文正在参加「金石计划」

flag:每月至少产出三篇高质量文章~

之前基于 React18+TS4.x+Webpack5 从0到1搭建了一个 React 基本项目架子,并在 npm 上发布了我们的脚手架:

前两天,一位 jy [长路漫漫且灿灿] (名怪好听的😁)告诉我说引入 Tailwind 出错:

image.png

作为一个勤快的博主,怎么能不极速解决观众老爷的烦恼呢?然后早上6点就爬起来,看看问题究竟在哪 ~

image.png

1、怎么欢(fan)车了!

说在前面:这里需要声明一下,之所以遇到这个坑是因为,我基于“从0到1搭建React18+TS4.x+Webpack5项目” 中引入 Tailwind。如果你正常使用官方脚手架 —— CRA 来搭建项目,并且按照 Tailwind 官方给出的教程来安装使用,是不会遇到这个问题,大家可以不用担心。

据 jy 说,我按照官方给出的 installation/using-postcss 教程,安装并使用:

image.png

预期应该是这样的:

image.png

但实际是这样的,并没有任何效果:

image.png

果然如之前评论区 jy 所说的(见上图),出问题了!

2、问题出在哪?

我首先对项目进行了 build,然后 serve -S dist 在浏览器中查看了一下 build产物

image.png

我发现 Tailwindclass nameModule 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"。以下是每个值的含义:

  1. local :默认值。CSS 模块会使用本地作用域策略进行处理。这意味着每个类名都会被转换为一个唯一的名称,该名称作用域限定于该模块。当希望避免不同组件之间的命名冲突时,这种模式对于构建应用程序非常有用。在此模式下,生成的类名仅在定义它们的组件中具有本地作用域。例如,如果你有一个名为 "Button" 的组件并在其 CSS 中定义了一个类名 "primary",则生成的类名可能类似于 "Button__primary__3y78d"。这样,"primary" 类名仅适用于 "Button" 组件,不会影响具有 "primary" 类名的任何其他组件。

  2. icss:这代表 "Interoperable CSS",CSS 模块会使用 Interoperable CSS (ICSS) 规范进行处理。在此模式下,使用 ICSS 标准加载 CSS 模块,这允许更高级的功能,例如可组合的 CSS 和动态主题。这意味着模块导出一个普通对象,其中包含从本地类名到全局类名的映射,可以用于将样式导入到其他模块中。这种模式对于构建可由其他应用程序消费的库非常有用。

  3. global:在此模式下,CSS 类名是全局的,可以在整个应用程序中使用。这对于定义在多个组件中使用的实用程序类很有用。"global" 模式生成的类名不限于特定组件的范围。这对于定义可以在不同组件之间重复使用的实用类非常有用。例如,如果你有一个名为 "text-center" 的类,用于水平居中文本,可以定义它一次,然后在不同组件中使用它,而无需每次重新定义它。

  4. pure"pure" 模式类似于 "local" 模式,但它还从最终输出中删除未使用的 CSS 类名。这可以帮助减小 CSS 文件的大小并提高性能。


而且我发现在 CRA 创建的 react 项目中,也对 pure cssmodule css 做了细节的处理:

image.png

于是,我将 webpack.base.ts 里面关于样式的 options 修改了一下:

image.png

并引入了 Tailwind 必要的配置:

image.png

完整的 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 写的标签:

image.png

重启项目,便看到效果了:

image.png

build 后的 className 也没有被重命名为带模块前缀的样子:

image.png

在开发的时候,如果你不希望每次引入 Tailwind 样式之后,都要重新run一次,可以使用官方提供的这种方式

image.png

在控制台执行,它会监听你文件的变化,然后自动更新样式表文件:

npx tailwindcss -i ./src/tailwind.css -c ./tailwind.config.js -o ./src/index.css --watch 

你可以使用 --watch--w 标志来启动一个观察进程,并在你做任何修改时自动重建你的CSS。

不过这种建监听的方式有点 stupid someway,后续再看看有啥更好办法吧 ~ 如果掘友们有好的 idea,欢迎评论区留言讨论。

然后就可以在项目中愉快地使用 Tailwind 了~

image.png

源码放在:react18-ts4-webpack5-starter - 分支:cha-03-tailwind

end~

欢迎关注之前的几篇文章: