MVVM+Monorepo工程设计

MVVM+Monorepo工程设计

背景

为了实现视图与逻辑分离,增强项目的可维护性和提高复用度,开放平台项目采用mvvm模式 + monorepo拆包的方式搭建应用工程。

MVVM介绍

MVVM(Model-View-ViewModel)是一种软件架构设计模式,非常适合于编写GUI的项目。MVVM架构模式将工程分为三层:视图层View,视图模型层ViewModel,模型层Model。

  • View:视图层仅负责界面的展示和交互逻辑,界面中可变部分抽象为视图模型ViewModel,View层使用ViewModel层提供的数据进行界面渲染,同时View层和ViewModel层需建立起一种双向绑定的机制,即:当ViewModel中的值发生了变化,界面会自动更新。

  • ViewModel:提取View中可变的数据部分,抽象为ViewModel。ViewModel是View和Model层的一个中间处理层,常常在该层组装Model层的数据和处理业务逻辑。ViewModel层会向View层提供更新ViewModel的方法,并且当ViewModel层的数据发生变化时,会自动通知View层进行更新操作。

  • Model:数据模型层,通常维护着系统所需要的底层数据,如:数据库的数据,远程调用的数据,静态文件中的数据等等。Model层向ViewModel层暴露操作数据的方法。

image.png

依赖关系:View 依赖于 ViewModel,ViewModel 依赖于 Model。VIew层不可直接依赖于Model层

关键机制:View层和ViewModel层需要建立起一种双向数据绑定的管理,当ViewModel层中的数据发生变化后,自动通知到视图层的更新(vue和react都能很好的支持数据与视图的双向绑定)

Monorepo介绍

Monorepo 其实不是一个新的概念,在软件工程领域,它已经有着十多年的历史了。概念上很好理解,就是把多个项目放在一个仓库里面,相对立的是传统的 MultiRepo 模式,即每个项目对应一个单独的仓库来分散管理。

现代的前端工程已经越来越离不开 Monorepo 了,无论是业务代码还是工具库,越来越多的项目已经采用 Monorepo 的方式来进行开发。Google 宁愿把所有的代码都放在一个 Monorepo 工程下面,Vue 3、Yarn、Npm7 等等知名开源项目的源码也是采用 Monorepo 的方式来进行管理的。

一般 Monorepo 的目录如下所示,在 packages 存放多个子项目,并且每个子项目都有自己的package.json:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

技术选型

View层

react:负责界面渲染和事件交互

umi:umi负责工程构建和路由管理等

ViewModel层

zustand:状态管理工具,store中存储ViewModel的数据和Actions。zustand是一个小巧、高效、可扩展的状态管理库。

A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn’t boilerplatey or opinionated, but has enough convention to be explicit and flux-like.

Don’t disregard it because it’s cute, it has claws! Lots of time was spent to deal with common pitfalls, like the dreaded zombie child problemReact concurrency, and context loss between mixed renderers. It may be the one state manager in the React space that gets all of these right.

You can try a live demo here.

Github:github.com/pmndrs/zust…

官方文档:docs.pmnd.rs/zustand/get…

Model层

axios: 网络请求库

monorepo选择

monorepo的方案有很多可选项,如下

简单工具:

专业工具:

由于业务项目不复杂,所以使用最简单成本低的pnpm workspaces方案即可

工程目录结构设计

目录结构

...               根目录主要包含一些项目配置,如eslint、pretter、git、commitlint等
packages/         主要业务代码包
├── common        common包含model,view,view-model包公共的依赖,如utils,const等
├── model         model包含获取数据的方法,数据可从网络服务,静态数据,文件等获得
├── view          view包含界面相关的代码,只负责根据viewmodel的数据渲染界面
└── view-model    viewmodel包含vm的数据仓库,使用zustand作为状态管理工具

image

包的依赖管理

view包 依赖于 viewmodel和common

view-model包 依赖于model和common

model 依赖于 common

注意: 依赖关系不可乱,遵循以上原则,如view不可直接依赖于model

基于pnpm workspace的monorepo

packages:
  - 'packages/*'

model包依赖于common包

{

  "name": "uop-model",
  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "test": "echo \"Error: no test specified\" && exit 1"

  },

  "author": "",

  "license": "ISC",

  "dependencies": {

    "uop-common": "workspace:1.0.0"
  }
}

viewModel包依赖于model和common包

{

  "name": "uop-view-model",
  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "test": "echo \"Error: no test specified\" && exit 1"

  },

  "author": "",

  "license": "ISC",

  "dependencies": {

    "uop-common": "workspace:1.0.0",
    "uop-model": "workspace:1.0.0",
    "zustand": "4.3.8"
  }
}

view包依赖于viewModel包和common包

{
  "name": "uop-web",
  "private": true,
  "scripts": {
  ...省略
  },
  "dependencies": {
     ...省略
    "uop-common": "workspace:1.0.0",
    "uop-view-model": "workspace:1.0.0",
  },
  "devDependencies": {
   ...省略
  }
}

命名规范

 ViewModel层

  • 数据变量以 VM 结尾 ,如apiDetailVM

  • 方法以 VMAction 结尾,如 getApiDetailVMAction

  • 对应的ts类型声明也遵循以上定义,如type ApiCommonParamsVM

示例

// View Model
export const useApiTreeViewModel = create<ApiTreeViewModel>((set) => {
  return {
    // 数据以VM命名
    apiTreeVM: [],
    // 方法以VMAction命名
    getApiTreeVMAction: () => {},
  };
});

Model层

  • 数据模型层的数据或方法以 DM结尾,如:getApiDetailDM

优点

定义规范的名称,在依赖方引入的时候,一眼就可辨别出该变量或方法是viewmodel还是model,可避免错误的发生

代码示例

Model层

定义了获取数据模型的方法,从服务端接口获取接口树的数据

import http from 'uop-common/http';

type ApiTreeItem = {
  key?: number | string;
  title?: string;
  label: string;
  id?: string;
  version?: string;

  name?: string;
  children?: ApiTreeItem[];
};
export interface ApiTreeResDM {
  appId: string;
  gatewayUrl: string;
  services: ApiTreeItem[];
  urlProd: string;
  urlTest: string;
}

export const getApiTreeDM = (): Promise<ApiTreeResDM> => {
  return http.get('/ifs/uop/v1/web/doc/menus');
};

ViewModel层

ApiTreeViewModel是由状态管理工具zustand创建的store,其中存储了apiTree的数据和操作方法,ViewModel层的数据又是从Model层获取得到的。

import { ApiTreeResDM, getApiTreeDM } from 'uop-model/api-info';
import { create } from 'zustand';

type ApiTreeItem = {
  key: string;
  title: string;
  id: string;
  version?: string;

  children: ApiTreeItem[];
};
export type ApiTreeVM = ApiTreeItem[];


const handleTreeData = (data: ApiTreeResDM['services'] = []): ApiTreeVM => {
  for (const item of data) {
    item.key = item.id || String(Math.random());
    item.title = item.label;
    if (item.children) {
      handleTreeData(item.children);
    }
  }
  return data as ApiTreeVM;
};


type ApiTreeViewModel = {
  apiTreeVM: ApiTreeVM;
  getApiTreeVMAction: () => void;
};

export const useApiTreeViewModel = create<ApiTreeViewModel>((set) => {
  return {
    apiTreeVM: [],
    getApiTreeVMAction: () => {
      getApiTreeDM().then((res) => {
        set(() => ({ apiTreeVM: handleTreeData(res.services) }));
      });
    },
  };
});

View层

界面层绑定ViewModel层提供的数据,并调用ViewModel层提供的方法更新VeiwModel中的数据,进而界面会随着绑定的数据的变化而更新。

import { CaretRightOutlined } from '@hammer/icons';
import { history } from '@umijs/max';
import { Menu } from 'hammer';
import { useEffect, useState } from 'react';
import { ApiTreeVM, useApiTreeViewModel } from 'uop-view-model/api-tree';

interface Props {
  id: string;
  onChange?: (id: string) => void;
}



export const ApiTree = (props: Props) => {
  const { id } = props;
  const { onChange } = props;
  const { apiTreeVM = [], getApiTreeVMAction } = useApiTreeViewModel(
    (state) => state,
  );
  
  useEffect(() => {
    getApiTreeVMAction();
  }, []);
  
  return (
    <Menu
      items={apiTreeVM}
      mode="inline"
    ></Menu>
  );
};

总结

MVVM是一种将 界面的展示交互 与 业务逻辑 分离的架构模式,Monorepo又进一步将每一层拆分为单独的包,给我们的工程带来了如下几个优点

  • 提高可维护性:清晰的分层架构,每一层的职责分明,逻辑清晰。view层只负责界面的展示和交互,view Model层负责业务逻辑和数据处理,model层负责调用远程服务

  • 提高复用度:假如我们已经有一个pc版本的web工程,需要做一个移动版本的工程,那么仅需要编写view层即可,viewModel和model都可以直接复用

  • 提效:对于前端工程来说,model层大部分是服务调用,所以model层可使用工具直接生成,无需手动编码。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYpMCcXE' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片