React native 项目函数式编程的背后-另类的架构InversifyJS 依赖注入(DI)

引言

在复杂的现代React Native应用开发中,组件之间的依赖关系管理一直是一个挑战。随着项目规模的增长,代码变得越来越难以维护、测试和扩展。依赖注入(DI)作为一种设计模式,旨在解决这些问题,而InversifyJS则是TypeScript/JavaScript生态系统中最强大的DI容器之一。

为什么需要依赖注入?

在传统开发模式中,组件通常直接创建和管理其依赖项,导致高耦合度和难以测试的代码。例如:

typescript 复制代码
// 高耦合的代码
class UserViewModel {
  private userService = new UserService(); // 直接创建依赖
  
  async getUsers() {
    return this.userService.fetchUsers();
  }
}

这种方式存在以下痛点:

  1. 高耦合度:组件与其具体依赖紧密绑定
  2. 难以测试:无法轻易替换真实依赖为测试替身
  3. 缺乏灵活性:切换实现需要修改多处代码
  4. 代码可读性差:真实项目中依赖关系复杂,难以追踪
  5. 重用性差:组件与其依赖的硬编码限制了重用性

函数式编程 vs 依赖注入:为什么在React Native中仍需InversifyJS?

有人可能会问:"React Native社区目前推荐函数式编程,为什么还要使用基于类的依赖注入方案?"

这是一个很好的问题。确实,React和React Native正越来越倾向于函数式组件和Hooks。但这并不意味着依赖注入和InversifyJS变得无关紧要,原因如下:

1. 业务逻辑与UI分离

函数式组件和Hooks主要关注UI层面的状态管理和副作用处理,而复杂应用的业务逻辑往往需要更结构化的方式来组织。InversifyJS允许我们将业务逻辑从UI组件中抽离,形成清晰的分层架构:

typescript 复制代码
// 使用DI的函数式组件
function UserListScreen() {
  // 通过DI获取ViewModel
  const viewModel = useInjection<UserViewModel>(TYPES.UserViewModel);
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    // 业务逻辑在ViewModel中,与UI完全分离
    viewModel.getUsers().then(setUsers);
  }, []);
  
  return (
    <FlatList
      data={users}
      renderItem={({item}) => <UserItem user={item} />}
    />
  );
}

2. 大型项目的可维护性

当项目规模增长时,纯函数式方法可能导致业务逻辑分散在各个组件中,难以追踪和维护。使用DI可以创建一个清晰的依赖图,帮助团队理解复杂系统的结构:

swift 复制代码
UI组件 → ViewModels → UseCases → Repositories → 数据源

3. 企业级应用的标准化

对于企业级应用,特别是多团队协作的项目,DI提供了一种标准化的方式来组织代码,使新加入的开发者能够快速理解项目结构。

4. 兼容函数式和类

InversifyJS完全可以与函数式组件和Hooks结合使用:

typescript 复制代码
// 创建自定义hook连接DI和函数式组件
export function useUserViewModel() {
  return useInjection<UserViewModel>(TYPES.UserViewModel);
}

// 在函数式组件中使用
function ProfileScreen() {
  const userViewModel = useUserViewModel();
  // ...
}

5. 测试优势

依赖注入大大简化了单元测试。通过模拟依赖,我们可以轻松测试逻辑而不涉及真实API调用或其他外部服务:

typescript 复制代码
// 测试变得简单
it('should fetch users', async () => {
  // 模拟userRepository
  const mockRepository = { getUsers: jest.fn().mockResolvedValue([testUser]) };
  
  // 注入模拟依赖
  container.bind(TYPES.UserRepository).toConstantValue(mockRepository);
  const viewModel = container.get<UserViewModel>(TYPES.UserViewModel);
  
  // 测试
  const result = await viewModel.getUsers();
  expect(result).toEqual([testUser]);
  expect(mockRepository.getUsers).toHaveBeenCalledTimes(1);
});

适用项目类型

InversifyJS特别适合以下类型的项目:

  1. 中大型React Native应用:随着功能增加,依赖关系变得复杂
  2. 企业级应用:需要可维护性和可测试性的长期项目
  3. 多人团队协作:清晰的依赖结构提高团队协作效率
  4. 需要高度模块化的项目:支持按功能划分和按需加载
  5. 使用TypeScript的项目:利用强类型优势实现完整的DI体验
  6. 需要灵活切换实现的项目:例如多环境配置或A/B测试
  7. 混合范式项目:同时使用函数式和面向对象编程的项目

典型项目结构

使用InversifyJS的React Native项目通常采用清晰的分层架构:

bash 复制代码
src/
├── app/                           # 应用核心
│   ├── App.tsx                    # 应用入口
│   └── di/                        # 依赖注入设置
│       ├── container.ts           # DI容器配置
│       ├── modules/               # 模块化DI配置
│       │   ├── repositoryModule.ts
│       │   ├── serviceModule.ts
│       │   └── viewModelModule.ts
│       ├── provider.tsx           # React DI Provider
│       └── types.ts               # DI类型标识符
│
├── presentation/                  # 表现层
│   ├── screens/                   # UI屏幕
│   │   ├── HomeScreen.tsx
│   │   └── ProfileScreen.tsx
│   ├── components/                # UI组件
│   ├── hooks/                     # 自定义Hooks
│   │   └── useViewModels.ts       # ViewModel接入点
│   └── viewmodels/                # 视图模型
│       ├── HomeViewModel.ts
│       └── ProfileViewModel.ts
│
├── domain/                        # 领域层
│   ├── usecases/                  # 用例
│   │   ├── GetUserUseCase.ts
│   │   └── UpdateProfileUseCase.ts
│   ├── entities/                  # 领域实体
│   └── repositories/              # 仓库接口
│       └── UserRepository.ts
│
└── data/                          # 数据层
    ├── repositories/              # 仓库实现
    │   ├── RemoteUserRepository.ts
    │   └── LocalUserRepository.ts
    ├── datasources/               # 数据源
    │   ├── api/                   # API客户端
    │   └── local/                 # 本地存储
    └── models/                    # 数据模型

这种结构基于"干净架构"(Clean Architecture)原则,每一层都有明确的责任。即使在函数式编程范式下,这种分层架构仍然非常有价值,因为它分离了关注点,使得代码更易于理解和维护。

通过将InversifyJS与函数式React组件结合,我们可以获得两个世界的最佳特性:函数式编程的简洁和可组合性,以及依赖注入的结构化和可测试性。这种方法特别适合那些开始简单但预计会随时间增长的项目,因为它提供了一个可扩展的基础,能够适应不断增长的复杂性。

在接下来的章节中,我们将深入探讨InversifyJS的核心概念、各种用法和高级技巧,帮助您在React Native项目中充分发挥依赖注入的优势。

1. 基本装饰器

@injectable()

将类标记为可注入,使其能够被依赖注入容器管理。

typescript 复制代码
import { injectable } from 'inversify';

@injectable()
class UserService {
  // 类实现...
}

@inject()

指定构造函数参数或属性需要注入哪个依赖。

typescript 复制代码
import { injectable, inject } from 'inversify';
import { TYPES } from './types';

@injectable()
class UserViewModel {
  constructor(
    @inject(TYPES.UserService) private userService: UserService
  ) {}
}

2. 注入方式

2.1 构造函数注入

最常用的注入方式,依赖在构造函数中声明。

typescript 复制代码
@injectable()
class ProductViewModel {
  constructor(
    @inject(TYPES.ProductService) private productService: ProductService,
    @inject(TYPES.Logger) private logger: Logger
  ) {}
}

2.2 属性注入

直接在类属性上注入依赖,适合可选依赖。

typescript 复制代码
@injectable()
class OrderService {
  @inject(TYPES.EmailService)
  private emailService!: EmailService;
  
  // 类实现...
}

2.3 方法注入

在方法参数上进行注入。

typescript 复制代码
@injectable()
class CheckoutService {
  processPayment(
    @inject(TYPES.PaymentGateway) paymentGateway: PaymentGateway,
    amount: number
  ) {
    // 实现...
  }
}

3. 生命周期选项

3.1 inSingletonScope()

整个应用生命周期内只创建一个实例,适合共享状态的服务。

typescript 复制代码
container.bind<AuthService>(TYPES.AuthService)
  .to(AuthService)
  .inSingletonScope();

3.2 inTransientScope()

每次请求都创建新实例,适合无状态服务。

typescript 复制代码
container.bind<OrderProcessor>(TYPES.OrderProcessor)
  .to(OrderProcessor)
  .inTransientScope();

3.3 inRequestScope()

在同一"请求"上下文中共享同一实例,常用于Web服务。

typescript 复制代码
container.bind<UserContext>(TYPES.UserContext)
  .to(UserContext)
  .inRequestScope();

4. 高级绑定方式

4.1 toConstantValue()

绑定到一个常量值。

typescript 复制代码
container.bind<string>(TYPES.ApiKey)
  .toConstantValue("your-api-key-here");
  
container.bind<number>(TYPES.MaxRetries)
  .toConstantValue(3);

4.2 toDynamicValue()

绑定到动态计算的值,每次请求都会执行函数计算。

typescript 复制代码
container.bind<Date>(TYPES.CurrentDate)
  .toDynamicValue(() => new Date());
  
container.bind<ApiClient>(TYPES.ApiClient)
  .toDynamicValue((context) => {
    const apiKey = context.container.get<string>(TYPES.ApiKey);
    return new ApiClient(apiKey);
  });

4.3 toFactory()

绑定到工厂函数,允许创建自定义对象。

typescript 复制代码
container.bind<() => Logger>(TYPES.LoggerFactory)
  .toFactory((context) => {
    return (logLevel: string) => {
      const logger = context.container.get<Logger>(TYPES.Logger);
      logger.setLevel(logLevel);
      return logger;
    };
  });

4.4 toProvider()

返回一个Promise的特殊工厂,适合异步创建对象。

typescript 复制代码
container.bind<interfaces.Provider<Database>>(TYPES.DatabaseProvider)
  .toProvider((context) => {
    return async () => {
      const config = context.container.get<DbConfig>(TYPES.DbConfig);
      const connection = await createConnection(config);
      return new Database(connection);
    };
  });

5. 条件绑定

5.1 when()与条件

5.1.1 whenTargetNamed()

当目标具有特定名称时应用绑定。

typescript 复制代码
container.bind<Repository>(TYPES.Repository)
  .to(LocalRepository)
  .whenTargetNamed('local');
  
container.bind<Repository>(TYPES.Repository)
  .to(RemoteRepository)
  .whenTargetNamed('remote');

// 使用
@injectable()
class DataService {
  constructor(
    @inject(TYPES.Repository) @named('local') private localRepo: Repository,
    @inject(TYPES.Repository) @named('remote') private remoteRepo: Repository
  ) {}
}

5.1.2 whenParentNamed()

当父依赖项具有特定名称时应用绑定。

typescript 复制代码
container.bind<Engine>(TYPES.Engine)
  .to(ElectricEngine)
  .whenParentNamed('tesla');
  
container.bind<Engine>(TYPES.Engine)
  .to(GasEngine)
  .whenParentNamed('toyota');

5.1.3 whenAnyAncestorIs()

当祖先依赖是特定类型时应用绑定。

typescript 复制代码
container.bind<Logger>(TYPES.Logger)
  .to(DetailedLogger)
  .whenAnyAncestorIs(AdminService);
  
container.bind<Logger>(TYPES.Logger)
  .to(SimpleLogger)
  .whenAnyAncestorIs(UserService);

5.1.4 whenAnyAncestorNamed()

当任何祖先依赖有特定名称时应用绑定。

typescript 复制代码
container.bind<Theme>(TYPES.Theme)
  .to(DarkTheme)
  .whenAnyAncestorNamed('dark-mode');
  
container.bind<Theme>(TYPES.Theme)
  .to(LightTheme)
  .whenAnyAncestorNamed('light-mode');

5.1.5 whenNoAncestorIs()

当没有祖先依赖是特定类型时应用绑定。

typescript 复制代码
container.bind<AuthStrategy>(TYPES.AuthStrategy)
  .to(DefaultAuthStrategy)
  .whenNoAncestorIs(AdminArea);

5.1.6 whenNoAncestorNamed()

当没有祖先依赖具有特定名称时应用绑定。

typescript 复制代码
container.bind<Logger>(TYPES.Logger)
  .to(ConsoleLogger)
  .whenNoAncestorNamed('silent-mode');

5.1.7 whenNoAncestorTagged()

当没有祖先依赖具有特定标签时应用绑定。

typescript 复制代码
container.bind<Notifier>(TYPES.Notifier)
  .to(EmailNotifier)
  .whenNoAncestorTagged('notification-type', 'silent');

5.1.8 custom() - 自定义条件

创建完全自定义的绑定条件。

typescript 复制代码
container.bind<DataSource>(TYPES.DataSource)
  .to(CloudDataSource)
  .when(request => {
    return process.env.NODE_ENV === 'production';
  });
  
container.bind<DataSource>(TYPES.DataSource)
  .to(MockDataSource)
  .when(request => {
    return process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
  });

6. 标签和元数据

6.1 @tagged 和 whenTargetTagged()

使用标签为绑定添加元数据。

typescript 复制代码
// 定义标签
const TAG = {
  API_VERSION: 'api-version',
  ENVIRONMENT: 'environment'
};

// 标记注入
@injectable()
class WeatherService {
  constructor(
    @inject(TYPES.ApiClient) 
    @tagged(TAG.API_VERSION, 'v2') 
    private apiClient: ApiClient
  ) {}
}

// 绑定时使用标签
container.bind<ApiClient>(TYPES.ApiClient)
  .to(ApiClientV1)
  .whenTargetTagged(TAG.API_VERSION, 'v1');
  
container.bind<ApiClient>(TYPES.ApiClient)
  .to(ApiClientV2)
  .whenTargetTagged(TAG.API_VERSION, 'v2');

6.2 @optional

标记一个依赖为可选的。如果没有找到绑定,不会抛出错误而是注入undefined。

typescript 复制代码
@injectable()
class AnalyticsService {
  constructor(
    @inject(TYPES.Tracker) @optional() private tracker?: Tracker
  ) {
    // tracker可能是undefined
    if (this.tracker) {
      this.tracker.init();
    }
  }
}

6.3 @multiInject

注入同一类型的多个实例。

typescript 复制代码
// 绑定多个实现
container.bind<Plugin>(TYPES.Plugin).to(LoggerPlugin);
container.bind<Plugin>(TYPES.Plugin).to(AuthPlugin);
container.bind<Plugin>(TYPES.Plugin).to(CachePlugin);

// 注入所有实现
@injectable()
class Application {
  constructor(
    @multiInject(TYPES.Plugin) private plugins: Plugin[]
  ) {
    // plugins是一个数组,包含所有绑定到TYPES.Plugin的实例
    this.plugins.forEach(plugin => plugin.initialize());
  }
}

7. 模块化容器

7.1 ContainerModule

将相关绑定组织到模块中,提高代码组织性。

typescript 复制代码
import { ContainerModule } from 'inversify';

// 仓库模块
export const repositoryModule = new ContainerModule((bind) => {
  bind<UserRepository>(TYPES.UserRepository).to(SqlUserRepository);
  bind<ProductRepository>(TYPES.ProductRepository).to(SqlProductRepository);
});

// 服务模块
export const serviceModule = new ContainerModule((bind) => {
  bind<UserService>(TYPES.UserService).to(UserService);
  bind<ProductService>(TYPES.ProductService).to(ProductService);
});

// 加载模块
const container = new Container();
container.load(repositoryModule, serviceModule);

7.2 AsyncContainerModule

支持异步初始化的模块。

typescript 复制代码
import { AsyncContainerModule } from 'inversify';

export const databaseModule = new AsyncContainerModule(async (bind) => {
  const connection = await createDatabaseConnection();
  bind<DatabaseConnection>(TYPES.DatabaseConnection)
    .toConstantValue(connection);
  
  bind<UserRepository>(TYPES.UserRepository)
    .to(SqlUserRepository);
});

// 异步加载
const container = new Container();
await container.loadAsync(databaseModule);

8. 在React Native中使用的高级技巧

8.1 自定义React Hooks

创建自定义hooks简化依赖注入的使用:

typescript 复制代码
// useViewModel.ts
import { useInjection } from '../di/provider';
import { TYPES } from '../di/types';

export function useUserViewModel() {
  return useInjection<UserViewModel>(TYPES.UserViewModel);
}

export function useProductViewModel() {
  return useInjection<ProductViewModel>(TYPES.ProductViewModel);
}

// 在组件中使用
function UserProfileScreen() {
  const userViewModel = useUserViewModel();
  // ...
}

8.2 懒加载容器绑定

提高应用性能,按需加载模块:

typescript 复制代码
// 创建懒加载模块
const lazyLoadedModule = (moduleImport: () => Promise<any>) => {
  let moduleLoaded = false;
  let moduleInstance: any;
  
  return new ContainerModule((bind) => {
    // 为每个需要懒加载的服务创建代理
    bind<HeavyService>(TYPES.HeavyService)
      .toDynamicValue(async () => {
        if (!moduleLoaded) {
          moduleInstance = await moduleImport();
          moduleLoaded = true;
        }
        return new moduleInstance.HeavyService();
      });
  });
};

// 使用
container.load(
  lazyLoadedModule(() => import('./heavy-module'))
);

8.3 环境特定绑定

为不同环境提供不同实现:

typescript 复制代码
const configureRepositories = (env: string) => new ContainerModule((bind) => {
  if (env === 'development') {
    bind<ApiClient>(TYPES.ApiClient).to(MockApiClient);
  } else {
    bind<ApiClient>(TYPES.ApiClient).to(ProductionApiClient);
  }
});

// 加载环境特定模块
container.load(configureRepositories(process.env.NODE_ENV));

8.4 集成MobX或Redux

将DI与状态管理结合:

typescript 复制代码
// MobX与Inversify结合
@injectable()
class UserStore {
  @observable users: User[] = [];
  
  constructor(
    @inject(TYPES.UserService) private userService: UserService
  ) {
    makeAutoObservable(this);
  }
  
  @action
  async loadUsers() {
    this.users = await this.userService.getAll();
  }
}

container.bind<UserStore>(TYPES.UserStore)
  .to(UserStore)
  .inSingletonScope();

9. 实际应用场景示例

9.1 API请求场景

typescript 复制代码
// 定义接口
interface ApiClient {
  get<T>(url: string): Promise<T>;
  post<T>(url: string, data: any): Promise<T>;
}

// 实现
@injectable()
class HttpApiClient implements ApiClient {
  constructor(
    @inject(TYPES.ApiConfig) private config: ApiConfig,
    @inject(TYPES.AuthService) private authService: AuthService
  ) {}

  async get<T>(url: string): Promise<T> {
    const token = await this.authService.getToken();
    const response = await fetch(`${this.config.baseUrl}${url}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    return response.json();
  }

  async post<T>(url: string, data: any): Promise<T> {
    const token = await this.authService.getToken();
    const response = await fetch(`${this.config.baseUrl}${url}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(data)
    });
    return response.json();
  }
}

// 在DI容器中配置
container.bind<ApiConfig>(TYPES.ApiConfig).toConstantValue({
  baseUrl: 'https://api.example.com/v1'
});
container.bind<ApiClient>(TYPES.ApiClient).to(HttpApiClient).inSingletonScope();

9.2 特性切换

typescript 复制代码
// 特性开关接口
interface FeatureToggle {
  isEnabled(featureName: string): boolean;
}

// 远程配置实现
@injectable()
class RemoteFeatureToggle implements FeatureToggle {
  constructor(
    @inject(TYPES.ConfigService) private configService: ConfigService
  ) {}

  isEnabled(featureName: string): boolean {
    return this.configService.getFeatureFlag(featureName);
  }
}

// 本地测试实现
@injectable()
class LocalFeatureToggle implements FeatureToggle {
  private enabledFeatures: Set<string>;
  
  constructor() {
    this.enabledFeatures = new Set(['darkMode', 'betaFeatures']);
  }

  isEnabled(featureName: string): boolean {
    return this.enabledFeatures.has(featureName);
  }
}

// 基于环境配置
if (__DEV__) {
  container.bind<FeatureToggle>(TYPES.FeatureToggle)
    .to(LocalFeatureToggle)
    .inSingletonScope();
} else {
  container.bind<FeatureToggle>(TYPES.FeatureToggle)
    .to(RemoteFeatureToggle)
    .inSingletonScope();
}

9.3 全面的ViewModel示例

typescript 复制代码
@injectable()
class ProductDetailViewModel {
  @observable product: Product | null = null;
  @observable isLoading: boolean = false;
  @observable error: string | null = null;
  @observable relatedProducts: Product[] = [];
  
  constructor(
    @inject(TYPES.ProductUseCase) private productUseCase: ProductUseCase,
    @inject(TYPES.AnalyticsService) private analytics: AnalyticsService,
    @inject(TYPES.ErrorReporter) @optional() private errorReporter?: ErrorReporter
  ) {
    makeAutoObservable(this);
  }
  
  @action
  async loadProduct(id: string) {
    try {
      this.isLoading = true;
      this.error = null;
      
      this.product = await this.productUseCase.getProductById(id);
      this.relatedProducts = await this.productUseCase.getRelatedProducts(id);
      
      this.analytics.trackEvent('product_viewed', { productId: id });
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to load product';
      
      if (this.errorReporter) {
        this.errorReporter.report(err);
      }
    } finally {
      this.isLoading = false;
    }
  }
  
  @action
  addToCart() {
    if (!this.product) return;
    
    this.productUseCase.addToCart(this.product.id);
    this.analytics.trackEvent('add_to_cart', { 
      productId: this.product.id,
      price: this.product.price
    });
  }
}

// 在容器中注册
container.bind<ProductDetailViewModel>(TYPES.ProductDetailViewModel)
  .to(ProductDetailViewModel);

// 在Screen中使用
function ProductDetailScreen({ route }) {
  const { productId } = route.params;
  const viewModel = useInjection<ProductDetailViewModel>(TYPES.ProductDetailViewModel);
  
  useEffect(() => {
    viewModel.loadProduct(productId);
  }, [productId]);
  
  if (viewModel.isLoading) {
    return <LoadingSpinner />;
  }
  
  if (viewModel.error) {
    return <ErrorView message={viewModel.error} />;
  }
  
  // 渲染产品详情...
}

10. 性能优化

10.1 绑定缓存

typescript 复制代码
// 开启缓存优化
container.options.enableBindingCache = true;

10.2 预先生成依赖图

typescript 复制代码
// 应用启动时预热容器
function preloadContainer() {
  // 预先解析关键服务,避免首次使用时的延迟
  container.get(TYPES.AuthService);
  container.get(TYPES.UserService);
  container.get(TYPES.SettingsService);
}

// 应用启动后调用
preloadContainer();

10.3 避免循环依赖

检测并解决循环依赖问题:

typescript 复制代码
// 开启循环依赖检测
container.options.enableDiagnostics = true;

// 使用属性注入解决循环依赖
@injectable()
class ServiceA {
  @inject(TYPES.ServiceB)
  public serviceB!: ServiceB;
  
  public doSomething() {
    // 实现...
  }
}

@injectable()
class ServiceB {
  @inject(TYPES.ServiceA)
  public serviceA!: ServiceA;
  
  public doSomethingElse() {
    // 实现...
  }
}

总结

InversifyJS提供了丰富而强大的依赖注入功能,可以帮助您构建灵活、可测试且可维护的React Native应用。通过合理使用不同的注入方式、生命周期管理和高级特性,您可以创建松耦合的模块化应用架构,简化组件间的依赖关系管理,提高代码质量和开发效率。

相关推荐
曾经的三心草4 小时前
微服务的编程测评系统13-我的竞赛列表-elasticSearch
windows·微服务·架构
久笙&5 小时前
对象存储解决方案:MinIO 的架构与代码实战
数据库·python·架构
L2ncE5 小时前
高并发场景数据与一致性的简单思考
java·后端·架构
一休哥助手5 小时前
Naive RAG:简单而高效的检索增强生成架构解析与实践指南
运维·人工智能·架构
小云数据库服务专线5 小时前
谈谈架构的内容
架构·数据库架构
HyggeBest5 小时前
Golang 并发原语 Sync Cond
后端·架构·go
初岘8 小时前
自动驾驶架构:人为接口与隐式特征的博弈
人工智能·架构·自动驾驶
冯志浩9 小时前
React Native 中 useEffect 的使用
react native·掘金·金石计划
已读不回14313 小时前
告别痛苦的主题切换!用一个插件解决 Tailwind CSS 多主题开发的所有烦恼
前端·架构