本文正在参加「金石计划」
flag:每月至少产出三篇高质量文章~
之前基于 React18+TS4.x+Webpack5
从0到1搭建了一个 React
基本项目架子,并在 npm 上发布了我们的脚手架:
前两天,一位 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 了~
源码放在:react18-ts4-webpack5-starter - 分支:cha-03-tailwind
end~
欢迎关注之前的几篇文章:
- 【ChatGPT】一文教你怎么编写清晰有效的(Prompt)提示词~
- GPT-4 炸裂成那样,前端真的还有机会吗?我是这么看的~
- 【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(一)项目初始化
- 【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(二)基础功能配置
- 【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(三)代码质量和git提交规范
- 【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(四)发布脚手架
- 【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(一)项目初始化、配置和日志
- 【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(二)连接数据库
- 【重学CSS】图文并茂!一次性搞懂 Flex 布局,实现“布局自由”~
- 【重学CSS】图文并茂!一次性搞懂 Grid 布局,实现“布局自由”~
- 【微前端】手把手教你从0到1实现基于Webpack5 模块联邦(Module Federation)的微前端~
- 【微前端】在造一个微前端轮子之前,你需要知道这些~
- 【React】万字长文!100+个让你事半功倍的常用 React Hooks 和工具包
- 【React】面试官:如何在页面刷新之后保持状态?看看你知道几种~
- 【2023】前端趋势前瞻,学起来!