嘿,大家好!👋 花了一段时间构建React应用后,我发现随着应用变大,保持一个整洁的架构变得至关重要。今天,我来分享一下我们在React中使用MVVM(M-V-VM)模式的经验,它帮我们团队省去了无数麻烦,让代码库更易于管理。
這兒有您需要知道的好處!来啦!🎯
1. 你的代码变得超级有组织
— 数据、逻辑和UI组件分离明确
— 代码的每一部分都有明确的任务并能胜任
— 再不用纠结“这个逻辑该放哪里”了
2. 测试变得简单
— 业务逻辑被隔离在ViewModel中
— UI组件完全是展示性的
— 可以独立测试每个部分,无需模拟整个系统
3. 超强化的复用性
— ViewModels 可在不同组件间重复使用
— 逻辑保持一致
— 减少复制粘贴,保持单一事实来源
4. 合乎逻辑的状态管理
— 应用程序中清晰的数据流
— 可预测的状态更新
— 当出现问题时更容易调试(问题总是会出现的,不是吗?)
让我们构建一个简单的电子商务产品列表页面,它带有过滤器和排序功能。这里是如何用MVVM(模型-视图-视图模型)这样构建。
目录结构    src/  
    ├── pages/  
    │ └── ProductsPage/  
    │ ├── index.tsx      # 主页面组件  
    │ ├── index.hook.ts  # 页面钩子  
    │ ├── index.store.ts # 状态管理逻辑  
    │ ├── ViewModel.ts   # ViewModel逻辑  
    │ ├── types.ts       # TypeScript类型定义  
    │ ├── components/   
    │ │ ├── ProductGrid/  
    │ │ ├── FilterPanel/  
    │ │ └── SortingOptions/  
    │ └── styles/    // models/Product.model.ts
    export interface Product {
        id: string; // 产品ID
        name: string; // 产品名称
        price: number; // 产品价格
        category: string; // 产品分类
        inStock: boolean; // 是否有库存
    }
    // 产品接口定义了产品的ID, 名称, 价格, 分类和是否有库存信息。
    export interface FilterOptions {
        category: string[]; // 分类选项
        minPrice: number; // 最小价格
        maxPrice: number; // 最大价格
        inStock: boolean; // 是否有库存
    }
    // 筛选项接口定义了分类选项, 最小价格, 最大价格和是否有库存信息。// pages/产品页面/index.store.ts
import { create } from ‘zustand’;
interface 产品页面状态接口 {
  产品: 产品[];
  筛选条件: 过滤选项;
  更新产品: (产品: 产品[]) => void;
  更新筛选条件: (筛选条件: 过滤选项) => void;
}
const 使用产品仓库 = create<产品页面状态接口>((更新) => ({
  产品: [],
  筛选条件: {
    类别: [],
    minPrice: 0,
    maxPrice: 1000,
    库存状态: false
  },
  更新产品: (产品) => 更新({ 产品 }),
  更新筛选条件: (筛选条件) => 更新({ 筛选条件 })
}));    // pages/ProductsPage/ViewModel.ts
    class ProductsViewModel {
      private store: ProductsStore;
      private uiStore: UIStore;
      constructor(store: ProductsStore, uiStore: UIStore) {
        this.store = store;
        this.uiStore = uiStore;
      }
      public async 获取产品() {
        try {
          this.uiStore.showLoader();
          const { data } = await ProductsAPI.getProducts(this.store.filters);
          this.store.setProducts(data);
        } catch (error) {
          toast.error('获取产品时出错');
        } finally {
          this.uiStore.hideLoader();
        }
      }
      public updateFilters(filters: Partial<FilterOptions>) {
        this.store.setFilters({
          ...this.store.filters,
          ...filters
        });
      }
      public shouldShowEmptyState(): boolean {
        return !this.uiStore.isLoading && this.getFilteredProducts().length === 0;
      }
      public shouldShowError(): boolean {
        return !!this.uiStore.error;
      }
      public shouldShowLoading(): boolean {
        return this.uiStore.isLoading;
      }
      public shouldShowProductDetails(): boolean {
        return !!this.uiStore.selectedProductId;
      }
    }    // pages/产品页面/index.hook.ts
    const useProductsPage = () => {
      const productsStore = useProductsStore();
      const uiStore = useUIStore();
      const viewModel = new ProductsViewModel(productsStore, uiStore);
      // isRefreshing 和 refreshDone 用于处理页面特有的情况,且位于视图模型之外
      return {
        viewModel,
        isRefreshing: uiStore.isRefreshing,
        refreshDone: () => uiStore.setRefreshing(false),
      };
    };// pages/ProductsPage/index.tsx
const ProductsPage: FC = () => {
  const { viewModel, isRefreshing, refreshDone } = useProductsPage();
  useEffect(() => {
    viewModel.fetchProducts();
  }, [viewModel]);
  useEffect(() => {
    if (isRefreshing) {
      viewModel.fetchProducts();
      refreshDone();
    }
  }, [isRefreshing]);
  return (
    <div className="products-page">
      <FilterPanel />
      <ProductGrid />
      <SortingOptions />
    </div>
  );
};    // 好
    class ProductsViewModel {
      fetchProducts() { /* … */ }
      updateFilters() { /* … */ }
      sortProducts() { /* … */ }
    }
    // 例如,坏 — 职责混淆
    class ProductsViewModel {
      fetchProducts() { /* … */ }
      updateUserProfile() { /* … */ }
      handleCheckout() { /* … */ }
    }// 使用 useEffect 来监听 viewModel 的变化。当 viewModel 发生变化时,会创建一个新的 AbortController 实例,并使用该实例的信号来调用 viewModel 的 fetchProducts 方法获取产品数据。在组件卸载时,会调用 controller 的 abort 方法来取消正在进行的请求。
useEffect(() => {  
  const controller = new AbortController();  
  viewModel.fetchProducts(controller.signal);  
  return () => controller.abort();  
}, [viewModel]);    // 好
    const viewModel = new ProductsViewModel(store);
    // 不好 — 这会破坏响应性
    const viewModel = useMemo(() => new ProductsViewModel(store), [store]);1. 更多的样板文件
— 需要编写更多的初始代码
— 需要处理更多的文件
— 新成员的学习曲线更陡峭
2. 可能有些过分
— 对于简单的 CRUD 应用来说,这可能有些过分
— 小项目可能无法充分展现其优势
— 正确设置需要一定的时间
3. 团队一致同意是必要的
— 每个人都需要理解和遵守这个模式
— 需要一致的规范
— 文档变得非常重要
React中的MVVM模式并不是万能药,但它真的极大地提升了我们团队的效率和代码质量。从一个小功能开始试试,看看感觉怎么样。记住,目标是让代码更易维护,让生活更轻松!
大家可以在评论区留言提问哦。祝愉快编程!🚀
优化项请继续阅读更多内容,了解我们如何解决视图模型实例的重复问题,并让垃圾回收器保持愉快。
在这里找到实现MVVM架构的代码库here,不过建议你先读一下系列中的下一篇再动手。
通俗易懂 🚀感谢你加入我们__In Plain English_社区!在你离开前:
- 记得为作者鼓掌👏️并关注她/他
- 关注我们: X | LinkedIn | YouTube | Discord | 电子报 | 节目
- 在Differ上免费创建一个AI驱动博客。
- 更多内容请看PlainEnglish.io
共同学习,写下你的评论
评论加载中...
作者其他优质文章
 
                 
            
 
			 
					 
					