Appearance
CSS Modules模块化
概述
CSS Modules是一种CSS模块化解决方案,它通过构建工具自动生成唯一的类名,从而实现CSS的局部作用域。这种方式避免了全局命名冲突,使组件样式更加可维护和可复用。本文将深入探讨CSS Modules的原理、用法和最佳实践。
CSS Modules基础
什么是CSS Modules
CSS Modules不是官方规范,而是一种构建步骤中的处理方式。它通过以下方式实现模块化:
- 每个CSS文件都是一个独立的模块
- 类名会被编译成唯一的哈希值
- 通过import导入后作为对象使用
- 完全避免全局作用域污染
基础使用
jsx
// Button.module.css
.button {
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #0056b3;
}
.primary {
background-color: #007bff;
}
.secondary {
background-color: #6c757d;
}
.large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
// Button.jsx
import React from 'react';
import styles from './Button.module.css';
export function Button({ children, variant = 'primary', size = 'medium', onClick }) {
const buttonClass = `${styles.button} ${styles[variant]} ${styles[size]}`;
return (
<button className={buttonClass} onClick={onClick}>
{children}
</button>
);
}
// 编译后的类名示例
// .Button_button__2x3Kl
// .Button_primary__1a2b3
// .Button_large__4c5d6配置CSS Modules
javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
css: {
modules: {
// 自定义生成的类名格式
generateScopedName: '[name]__[local]___[hash:base64:5]',
// 哈希前缀
hashPrefix: 'prefix',
// 局部化模式
localsConvention: 'camelCase', // 支持驼峰命名
// 导出全局类名
exportGlobals: true
}
}
});
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly'
}
}
}
]
}
]
}
};
// next.config.js
module.exports = {
// Next.js 默认支持 CSS Modules
// 无需额外配置
};高级特性
组合(Composes)
css
/* Button.module.css */
.base {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.primary {
composes: base;
background-color: #007bff;
color: white;
}
.secondary {
composes: base;
background-color: #6c757d;
color: white;
}
.outline {
composes: base;
background-color: transparent;
border: 2px solid #007bff;
color: #007bff;
}
/* 从其他文件组合 */
.iconButton {
composes: base from './common.module.css';
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
/* 组合多个类 */
.primaryLarge {
composes: primary;
composes: large from './sizes.module.css';
}
// 使用组合后的类
import styles from './Button.module.css';
function Button({ variant = 'primary' }) {
return (
<button className={styles[variant]}>
Click me
</button>
);
}全局类名
css
/* styles.module.css */
/* 局部类名(默认) */
.container {
max-width: 1200px;
margin: 0 auto;
}
/* 全局类名 */
:global(.global-class) {
color: red;
}
/* 混合使用 */
.wrapper :global(.ant-btn) {
margin-right: 8px;
}
/* 全局选择器块 */
:global {
.global-header {
position: fixed;
top: 0;
width: 100%;
}
.global-footer {
margin-top: 2rem;
}
}
/* 本地与全局切换 */
:local(.local-class) {
/* 明确指定为局部类名 */
}
// 使用示例
import styles from './styles.module.css';
function Component() {
return (
<div className={styles.container}>
<div className="global-class">全局样式</div>
<div className={styles.wrapper}>
<button className="ant-btn">Ant Design Button</button>
</div>
</div>
);
}CSS变量集成
css
/* theme.module.css */
.light {
--primary-color: #007bff;
--background-color: #ffffff;
--text-color: #333333;
--border-color: #dee2e6;
}
.dark {
--primary-color: #4dabf7;
--background-color: #1a1a1a;
--text-color: #f5f5f5;
--border-color: #404040;
}
.card {
background-color: var(--background-color);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 1rem;
border-radius: 8px;
}
.button {
background-color: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
}
// 使用主题
import React, { useState } from 'react';
import styles from './theme.module.css';
function ThemedComponent() {
const [theme, setTheme] = useState('light');
return (
<div className={styles[theme]}>
<div className={styles.card}>
<h2>Themed Card</h2>
<button
className={styles.button}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Toggle Theme
</button>
</div>
</div>
);
}动态类名处理
jsx
// 使用模板字符串
import styles from './Component.module.css';
function Component({ active, disabled, size }) {
const className = `
${styles.base}
${active ? styles.active : ''}
${disabled ? styles.disabled : ''}
${styles[size]}
`.trim();
return <div className={className}>Content</div>;
}
// 使用数组和filter
function Component({ active, disabled, size }) {
const classes = [
styles.base,
active && styles.active,
disabled && styles.disabled,
styles[size]
].filter(Boolean).join(' ');
return <div className={classes}>Content</div>;
}
// 使用对象方式
function Component({ active, disabled }) {
const classMap = {
[styles.base]: true,
[styles.active]: active,
[styles.disabled]: disabled
};
const className = Object.keys(classMap)
.filter(key => classMap[key])
.join(' ');
return <div className={className}>Content</div>;
}
// 使用classnames库
import classNames from 'classnames';
import styles from './Component.module.css';
function Component({ active, disabled, size = 'medium' }) {
return (
<div className={classNames(
styles.base,
{
[styles.active]: active,
[styles.disabled]: disabled
},
styles[size]
)}>
Content
</div>
);
}
// 使用clsx库(更轻量)
import clsx from 'clsx';
import styles from './Component.module.css';
function Component({ active, disabled, size }) {
return (
<div className={clsx(
styles.base,
active && styles.active,
disabled && styles.disabled,
styles[size]
)}>
Content
</div>
);
}工具函数封装
创建样式工具
javascript
// utils/styles.js
// 1. 样式组合工具
export function combineStyles(...classes) {
return classes.filter(Boolean).join(' ');
}
// 2. 条件样式工具
export function conditionalStyle(styles, conditions) {
return Object.keys(conditions)
.filter(key => conditions[key])
.map(key => styles[key])
.filter(Boolean)
.join(' ');
}
// 3. 变体样式工具
export function variantStyle(styles, variant, defaultVariant = 'default') {
return styles[variant] || styles[defaultVariant] || '';
}
// 4. 组合工具类
export function createStyleComposer(styles) {
return {
base: (...baseClasses) => {
const base = baseClasses.map(c => styles[c]).filter(Boolean).join(' ');
return {
with: (...additionalClasses) => {
const additional = additionalClasses.map(c => styles[c]).filter(Boolean).join(' ');
return `${base} ${additional}`.trim();
},
when: (condition, className) => {
const conditional = condition ? styles[className] : '';
return `${base} ${conditional}`.trim();
},
variant: (variantName) => {
const variant = styles[variantName] || '';
return `${base} ${variant}`.trim();
}
};
}
};
}
// 使用示例
import styles from './Button.module.css';
import { combineStyles, conditionalStyle, createStyleComposer } from './utils/styles';
function Button({ variant, size, active, disabled, children }) {
// 方式1:基础组合
const className1 = combineStyles(
styles.button,
styles[variant],
styles[size],
active && styles.active,
disabled && styles.disabled
);
// 方式2:条件组合
const className2 = conditionalStyle(styles, {
button: true,
[variant]: true,
[size]: true,
active: active,
disabled: disabled
});
// 方式3:链式调用
const composer = createStyleComposer(styles);
const className3 = composer
.base('button')
.variant(variant)
.with(size)
.when(active, 'active')
.when(disabled, 'disabled');
return <button className={className3}>{children}</button>;
}类型安全的样式
typescript
// Button.module.css.d.ts (自动生成或手动创建)
declare const styles: {
readonly button: string;
readonly primary: string;
readonly secondary: string;
readonly large: string;
readonly small: string;
readonly active: string;
readonly disabled: string;
};
export default styles;
// Button.tsx
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps {
variant?: keyof Pick<typeof styles, 'primary' | 'secondary'>;
size?: keyof Pick<typeof styles, 'large' | 'small'>;
active?: boolean;
disabled?: boolean;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'medium',
active,
disabled,
children
}: ButtonProps) {
return (
<button
className={`${styles.button} ${styles[variant]} ${size !== 'medium' ? styles[size] : ''}`}
disabled={disabled}
>
{children}
</button>
);
}
// 使用typed-css-modules自动生成类型
// npm install -D typed-css-modules
// package.json
{
"scripts": {
"generate-css-types": "tcm src/**/*.module.css"
}
}样式复用策略
共享样式文件
css
/* common.module.css */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.flexCenter {
display: flex;
justify-content: center;
align-items: center;
}
.flexBetween {
display: flex;
justify-content: space-between;
align-items: center;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1rem;
}
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
/* ProductCard.module.css */
.productCard {
composes: card from './common.module.css';
max-width: 300px;
}
.header {
composes: flexBetween from './common.module.css';
margin-bottom: 1rem;
}
.addButton {
composes: button from './common.module.css';
background: #007bff;
color: white;
}
/* UserProfile.module.css */
.profileCard {
composes: card from './common.module.css';
max-width: 400px;
}
.profileHeader {
composes: flexCenter from './common.module.css';
flex-direction: column;
padding: 2rem;
}主题系统
css
/* themes/variables.module.css */
.lightTheme {
--color-primary: #007bff;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #dc3545;
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.darkTheme {
--color-primary: #4dabf7;
--color-secondary: #adb5bd;
--color-success: #51cf66;
--color-danger: #ff6b6b;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--border-color: #404040;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* components/Card.module.css */
.card {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
padding: 1.5rem;
border-radius: 8px;
}
.title {
color: var(--text-primary);
margin-bottom: 1rem;
}
.description {
color: var(--text-secondary);
line-height: 1.6;
}
// ThemeProvider.jsx
import React, { createContext, useContext, useState } from 'react';
import themeStyles from './themes/variables.module.css';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const themeClass = theme === 'light'
? themeStyles.lightTheme
: themeStyles.darkTheme;
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={themeClass}>
{children}
</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}响应式样式复用
css
/* responsive.module.css */
.responsiveContainer {
width: 100%;
padding: 0 1rem;
}
@media (min-width: 768px) {
.responsiveContainer {
padding: 0 2rem;
}
}
@media (min-width: 1024px) {
.responsiveContainer {
max-width: 1200px;
margin: 0 auto;
}
}
.responsiveGrid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.responsiveGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.responsiveGrid {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
.hideOnMobile {
display: none;
}
@media (min-width: 768px) {
.hideOnMobile {
display: block;
}
}
.showOnMobile {
display: block;
}
@media (min-width: 768px) {
.showOnMobile {
display: none;
}
}
/* 使用响应式复用 */
.productGrid {
composes: responsiveContainer from './responsive.module.css';
composes: responsiveGrid from './responsive.module.css';
}与预处理器集成
SASS/SCSS Modules
scss
// Button.module.scss
$primary-color: #007bff;
$secondary-color: #6c757d;
$border-radius: 4px;
// 混合宏
@mixin button-base {
padding: 0.5rem 1rem;
border: none;
border-radius: $border-radius;
cursor: pointer;
transition: all 0.3s ease;
}
// 函数
@function darken-color($color, $amount: 10%) {
@return darken($color, $amount);
}
.button {
@include button-base;
&Primary {
background-color: $primary-color;
color: white;
&:hover {
background-color: darken-color($primary-color);
}
}
&Secondary {
background-color: $secondary-color;
color: white;
&:hover {
background-color: darken-color($secondary-color);
}
}
&Large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
&Small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
}
// 使用嵌套
.card {
background: white;
padding: 1rem;
border-radius: 8px;
.header {
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
margin-bottom: 1rem;
.title {
font-size: 1.25rem;
font-weight: 600;
}
}
.body {
line-height: 1.6;
}
.footer {
border-top: 1px solid #eee;
padding-top: 0.5rem;
margin-top: 1rem;
}
}
// variables.module.scss
$colors: (
primary: #007bff,
secondary: #6c757d,
success: #28a745,
danger: #dc3545
);
@function color($name) {
@return map-get($colors, $name);
}
.primaryText {
color: color(primary);
}
.successButton {
background-color: color(success);
}Less Modules
less
// Button.module.less
@primary-color: #007bff;
@border-radius: 4px;
// 混合
.button-base() {
padding: 0.5rem 1rem;
border: none;
border-radius: @border-radius;
cursor: pointer;
transition: all 0.3s ease;
}
.button {
.button-base();
&-primary {
background-color: @primary-color;
color: white;
&:hover {
background-color: darken(@primary-color, 10%);
}
}
&-large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
}
// 使用守卫
.text-size(@size) when (@size = small) {
font-size: 0.875rem;
}
.text-size(@size) when (@size = large) {
font-size: 1.25rem;
}
.textSmall {
.text-size(small);
}
.textLarge {
.text-size(large);
}实战案例
完整的卡片组件
scss
// Card.module.scss
.card {
position: relative;
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&Horizontal {
flex-direction: row;
}
&Elevated {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
}
.image {
width: 100%;
height: 200px;
object-fit: cover;
.cardHorizontal & {
width: 200px;
height: auto;
}
}
.content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.header {
margin-bottom: 1rem;
}
.title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: #212529;
}
.subtitle {
font-size: 0.875rem;
color: #6c757d;
margin: 0;
}
.body {
flex: 1;
line-height: 1.6;
color: #495057;
margin-bottom: 1rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
}
.actions {
display: flex;
gap: 0.5rem;
}
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.3s ease;
&Primary {
background: #007bff;
color: white;
&:hover {
background: #0056b3;
}
}
&Secondary {
background: transparent;
color: #007bff;
border: 1px solid #007bff;
&:hover {
background: #007bff;
color: white;
}
}
}
// Card.jsx
import React from 'react';
import styles from './Card.module.scss';
import clsx from 'clsx';
export function Card({
image,
title,
subtitle,
children,
actions,
horizontal = false,
elevated = false
}) {
return (
<div className={clsx(
styles.card,
horizontal && styles.cardHorizontal,
elevated && styles.cardElevated
)}>
{image && (
<img src={image} alt={title} className={styles.image} />
)}
<div className={styles.content}>
{(title || subtitle) && (
<div className={styles.header}>
{title && <h3 className={styles.title}>{title}</h3>}
{subtitle && <p className={styles.subtitle}>{subtitle}</p>}
</div>
)}
<div className={styles.body}>
{children}
</div>
{actions && (
<div className={styles.footer}>
<div className={styles.actions}>
{actions.map((action, index) => (
<button
key={index}
className={clsx(
styles.button,
action.primary ? styles.buttonPrimary : styles.buttonSecondary
)}
onClick={action.onClick}
>
{action.label}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}
// 使用示例
function App() {
return (
<Card
image="https://example.com/image.jpg"
title="Card Title"
subtitle="Card Subtitle"
elevated
actions={[
{ label: 'Cancel', onClick: () => {} },
{ label: 'Confirm', primary: true, onClick: () => {} }
]}
>
<p>This is the card content</p>
</Card>
);
}表单组件系统
scss
// Form.module.scss
.form {
width: 100%;
max-width: 500px;
}
.formGroup {
margin-bottom: 1.5rem;
&:last-child {
margin-bottom: 0;
}
}
.label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #212529;
&Required::after {
content: '*';
color: #dc3545;
margin-left: 0.25rem;
}
}
.input,
.textarea,
.select {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
outline: 0;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
&:disabled {
background-color: #e9ecef;
opacity: 1;
}
&Error {
border-color: #dc3545;
&:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
}
&Success {
border-color: #28a745;
&:focus {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
}
}
.textarea {
min-height: 100px;
resize: vertical;
}
.helpText {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #6c757d;
}
.errorText {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #dc3545;
}
.checkbox,
.radio {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
input {
margin-right: 0.5rem;
}
}
.buttonGroup {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.submitButton {
padding: 0.5rem 1.5rem;
background: #007bff;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
&:hover {
background: #0056b3;
}
&:disabled {
background: #6c757d;
cursor: not-allowed;
}
}
// Form.jsx
import React, { useState } from 'react';
import styles from './Form.module.scss';
import clsx from 'clsx';
export function FormGroup({
label,
name,
type = 'text',
required,
error,
helpText,
...props
}) {
const inputClass = clsx(
styles.input,
error && styles.inputError
);
return (
<div className={styles.formGroup}>
<label
htmlFor={name}
className={clsx(styles.label, required && styles.labelRequired)}
>
{label}
</label>
{type === 'textarea' ? (
<textarea
id={name}
name={name}
className={clsx(styles.textarea, error && styles.textareaError)}
{...props}
/>
) : (
<input
id={name}
name={name}
type={type}
className={inputClass}
{...props}
/>
)}
{helpText && <span className={styles.helpText}>{helpText}</span>}
{error && <span className={styles.errorText}>{error}</span>}
</div>
);
}
export function Form({ children, onSubmit }) {
return (
<form className={styles.form} onSubmit={onSubmit}>
{children}
</form>
);
}布局组件
scss
// Layout.module.scss
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
position: sticky;
top: 0;
z-index: 1000;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.main {
flex: 1;
display: flex;
}
.sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #dee2e6;
@media (max-width: 768px) {
position: fixed;
left: -250px;
top: 0;
bottom: 0;
transition: left 0.3s ease;
z-index: 999;
&Open {
left: 0;
}
}
}
.content {
flex: 1;
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.footer {
background: #f8f9fa;
border-top: 1px solid #dee2e6;
padding: 2rem;
text-align: center;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
// Layout.jsx
import React, { useState } from 'react';
import styles from './Layout.module.scss';
import clsx from 'clsx';
export function Layout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className={styles.layout}>
<header className={styles.header}>
<div className={styles.container}>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
Menu
</button>
</div>
</header>
<main className={styles.main}>
<aside className={clsx(
styles.sidebar,
sidebarOpen && styles.sidebarOpen
)}>
Sidebar
</aside>
<div className={styles.content}>
{children}
</div>
</main>
<footer className={styles.footer}>
<div className={styles.container}>
Footer Content
</div>
</footer>
</div>
);
}性能优化
代码分割
jsx
// 动态导入CSS Modules
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// HeavyComponent.jsx
import React, { useEffect } from 'react';
function HeavyComponent() {
const [styles, setStyles] = React.useState({});
useEffect(() => {
import('./HeavyComponent.module.css').then(module => {
setStyles(module.default);
});
}, []);
return (
<div className={styles.container}>
Heavy Component
</div>
);
}样式压缩
javascript
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
css: {
modules: {
generateScopedName: (name, filename, css) => {
// 生产环境使用短类名
if (process.env.NODE_ENV === 'production') {
return `${name}_${hash(css)}`;
}
// 开发环境使用可读类名
return `${filename}_${name}`;
}
}
},
build: {
cssCodeSplit: true, // CSS代码分割
cssMinify: true // CSS压缩
}
});调试技巧
开发工具
jsx
// 开发环境显示原始类名
import styles from './Component.module.css';
function Component() {
if (process.env.NODE_ENV === 'development') {
console.log('Styles:', styles);
}
return <div className={styles.container}>Content</div>;
}
// 调试工具组件
function StyleDebugger({ styles }) {
if (process.env.NODE_ENV === 'production') {
return null;
}
return (
<div style={{
position: 'fixed',
bottom: 0,
right: 0,
background: 'black',
color: 'white',
padding: '1rem',
fontSize: '12px',
maxWidth: '300px',
overflow: 'auto'
}}>
<h4>CSS Modules Debug</h4>
<pre>{JSON.stringify(styles, null, 2)}</pre>
</div>
);
}最佳实践
1. 命名约定
- 使用有意义的类名
- 采用camelCase或kebab-case
- 避免过于通用的名称
- 使用语义化命名
2. 文件组织
- 一个组件一个样式文件
- 共享样式放在common目录
- 主题相关放在themes目录
- 保持文件结构清晰
3. 性能优化
- 使用composes减少重复
- 合理使用代码分割
- 压缩CSS输出
- 避免深层嵌套
4. 类型安全
- 使用TypeScript
- 生成类型定义文件
- 启用严格类型检查
总结
CSS Modules提供了一种优雅的CSS模块化解决方案:
- 局部作用域:避免全局污染
- 组合复用:通过composes实现样式复用
- 类型安全:与TypeScript完美集成
- 工具丰富:支持各种构建工具
- 性能优异:代码分割和压缩
- 易于维护:清晰的组织结构
掌握CSS Modules,能够写出更加模块化、可维护的React样式代码。