web前端用MVP模式搭建项目

MVP(Model-View-Presenter)是一种软件架构模式。

MVP模式组成

Model

  • 职责:负责数据管理和业务逻辑
  • 包含:数据存储、数据获取、业务规则、数据验证等
  • 特点:独立于用户界面,可以被多个View复用

View

  • 职责:负责用户界面的显示和用户交互
  • 包含:UI组件、用户输入处理、界面渲染等
  • 特点:被动的,不直接与Model交互,只与Presenter通信

Presenter

  • 职责:作为Model和View之间的中介
  • 包含:处理用户交互逻辑、格式化数据、控制View更新等
  • 特点:包含表示层逻辑,协调Model和View的交互

MVP模式数据流向

代码

用MVP模式实现CounterApp

model层

model应该有让调用方订阅和通知订阅方的能力。

ts 复制代码
export interface IModel<ModelData, ModelEvent> {
  addEventListener<T extends keyof ModelEvent>(type: T, callback: ModelEvent[T]): DisposeHandler;
  subscribeModelData(callback: (data: ModelData) => void): DisposeHandler;
  getModelData(): ModelData;
}

CounterModel的类型定义

ts 复制代码
export type CounterModelEvent = {
  "data:update": (data: number) => void;
};

export type CounterModelData = number

export interface ICounterModel extends IModel<CounterModelData, CounterModelEvent> {
    count : number
}

定义ICounterModel,面相接口编程。

CounterModel的代码实现

ts 复制代码
export class CounterModel implements ICounterModel
{
  eventemitter = new EventEmitter<CounterModelEvent>();

  private _count: number = 0;

  getModelData() {
    return this.count;
  }

  protected notifyDataUpdate(): void {
    this.eventemitter.emit("data:update", this.getModelData());
  }

  addEventListener<T extends keyof CounterModelEvent>(
    type: T,
    callback: CounterModelEvent[T]
  ): DisposeHandler {
    this.eventemitter.on(type, callback as any);
    return () => {
      this.eventemitter.off(type, callback);
    };
  }

  subscribeModelData(
    callback: (data: CounterModelData) => void
  ): DisposeHandler {
    return this.addEventListener("data:update", callback);
  }

  get count() {
    return this._count;
  }

  set count(value: number) {
    this._count = value;
    this.notifyDataUpdate();
  }
}

view层

view应该有订阅业务事件和渲染视图的能力

ts 复制代码
export interface IView<ViewEvent extends Record<string, Function>, ViewRenderData extends Object> {
    addEventListener<T extends keyof ViewEvent>(type: T, callback: ViewEvent[T]): () => void;
    render(data: ViewRenderData): void;
  
    destroy(): void;
  }

ICounterView类型定义

ts 复制代码
export type CounterViewEvent = {
  "click:increment": () => void;
  "click:decrement": () => void;
};

export type CounterViewRenderData = {
  count: number;
};

export interface ICounterView extends IView<CounterViewEvent, CounterViewRenderData> {}

CounterView代码

ts 复制代码
class CounterView implements ICounterView {
  private disposeList: DisposeHandler[] = [];
  private container: HTMLElement;
  private counterContainer!: HTMLDivElement;
  private eventemitter = new EventEmitter<CounterViewEvent>();

  constructor(container: HTMLElement) {
    this.container = container;
    this.init();
  }
  
  // 初始化视图绑定视图事件
  init() {
    this.disposeList.push(this.createDecrementButton());
    this.createCounterContainer()
    this.disposeList.push(this.createIncrementButton());
  }

  private createIncrementButton() {
    const incrementButton = document.createElement("button");
    incrementButton.textContent = "Increment";
    const handler = () => {
        this.eventemitter.emit("click:increment");
      };
    incrementButton.addEventListener("click",handler );

    return () => {
      incrementButton.removeEventListener("click", handler);
    };
  }

  private createCounterContainer() {
    const counterContainer = document.createElement("div");
    counterContainer.textContent = "0";
    this.container.appendChild(counterContainer);
  }

  private createDecrementButton() {
    const decrementButton = document.createElement("button");
    decrementButton.textContent = "Decrement";
    const handler = () => {
      this.eventemitter.emit("click:decrement");
    };
    decrementButton.addEventListener("click", handler);

    return () => {
      decrementButton.removeEventListener("click", handler);
    };
  }

  addEventListener<T extends keyof CounterViewEvent>(
    type: T,
    callback: CounterViewEvent[T]
  ): () => void {
    this.eventemitter.on(type, callback);
    return () => {
      this.eventemitter.off(type, callback);
    };
  }

  render(data: { count: number }) {
    this.counterContainer.textContent = data.count.toString();
  }

  destroy(): void {
    this.disposeList.forEach((dispose) => dispose());
    this.container.innerHTML = "";
  }
}

Presenter

CounterPresenter负责协调counterModel和counterView。

ts 复制代码
export class CounterPresenter implements IPresenter {
    private counterModel: ICounterModel;
    private counterView: ICounterView;

    constructor(counterModel: CounterModel, counterView: ICounterView) {
        this.counterModel = counterModel;
        this.counterView = counterView;
    }

    init() {
        this.bindViewEvent();
        this.bindModelEvent();
    }

    bindViewEvent() {
        this.counterView.addEventListener("click:increment", () => {
            this.counterModel.count += 1;
        });

        this.counterView.addEventListener("click:decrement", () => {
            this.counterModel.count -= 1;
        });
    }

    bindModelEvent() {
        this.counterModel.addEventListener("data:update", (data) => {
            this.counterView.render({ count: data });
        });
    }

    destroy(): void {
        this.counterModel = null;
        this.counterView = null;
    }
}

CounterApp负责初始化Model,View和Presenter,管理应用的生命周期,对外提供接口。

ts 复制代码
export class CounterApp {
  private counterModel!: CounterModel;
  private container: HTMLElement;
  private counterView!: CounterView;
  private counterPresenter!: CounterPresenter;

  constructor(container: HTMLElement) {
    this.container = container;
    this.createModels();
    this.createViews();
    this.createPresenters();
    this.initPresenters();
  }

  private createModels() {
    this.counterModel = new CounterModel(sameDetector);
  }

  private createViews() {
    this.counterView = new CounterView(this.container);
  }

  private createPresenters() {
    this.counterPresenter = new CounterPresenter(this.counterModel, this.counterView);
    
  }

  private initPresenters() {
    this.counterPresenter.init();
  }

  destroy() {
    this.counterPresenter.destroy();
    this.counterView.destroy();
    this.counterModel = null;
  }
}

用react的同学都知道,react有批处理机制,这里对model也做一个简单优化,避免一段逻辑触发多次model数据更新通知。

model接口修改添加两个方法

ts 复制代码
export interface IModel<ModelData, ModelEvent> {
  // ......
  startBatchUpdate(): void; // 开始批量更新
  finishBatchUpdate(): void; // 结束批量更新
}

数据层批处理的代码是类似的,行为可以让子类重写

BaseModel定义批处理流程

ts 复制代码
export abstract class BaseModel<ModelData, ModelEvent> implements IModel<ModelData, ModelEvent> {
  protected _isStartBatchUpdate: boolean = false; // 是否开始批量更新
  private beforeStartBatchUpdateModelData: ModelData | null = null; // 批量更新前的模型数据

  startBatchUpdate() {
    this._isStartBatchUpdate = true; // 开始批量更新
    this.beforeStartBatchUpdateModelData = this.getModelData(); // 记录批量更新前的模型数据
  }

  abstract getModelData(): ModelData;
  abstract isSame(oldData: ModelData, newData: ModelData): boolean;

  abstract addEventListener<T extends keyof ModelEvent>(type: T, callback: ModelEvent[T]): () => void;
  abstract subscribeModelData(callback: (data: ModelData) => void): () => void;

  finishBatchUpdate() {
    this._isStartBatchUpdate = false; // 结束批量更新
    if (this.beforeStartBatchUpdateModelData) {
      // 如果有批量更新前的模型数据,则比较是否有变化
      if (this.isSame(this.beforeStartBatchUpdateModelData, this.getModelData())) {
        return; // 如果没有变化,则不通知数据更新
      }
      this.beforeStartBatchUpdateModelData = null; // 清空批量更新前的模型数据
    }
    this.notifyDataUpdate(); // 通知数据更新
  }

  protected abstract notifyDataUpdate(): void; // 通知数据更新
}

这个例子view,model,presenter复杂度低,没有涉及多个view,model和presenter的情况,虽然比较简单,能通过这个应用理解MVP模式。

模式特点model层和view层解耦,presenter负责协调view层和model层。

优点

  • 依赖接口可以方便进行mock测试
  • model层独立于view层,可以被多个view层使用
  • 数据流向清晰

缺点

  • 简单应用容易复杂化
  • 事件驱动容易提高调试复杂度,观察者模式也容易提高调试复杂度
  • Presenter层承担太多职责容易导致臃肿
  • 团队开发成本比较高
相关推荐
qq_582943453 分钟前
html5侧边提示框
前端·javascript·html5
蓝倾10 分钟前
小红书获取关键词列表API接口详解
前端·后端·fastapi
初出茅庐的12 分钟前
uniapp - AI 聊天页面布局的实现
前端·vue.js·uni-app
chenbo100115 分钟前
http 路径解析规则,相对路径和绝对路径
javascript
山烛18 分钟前
小白学HTML,操作HTML网页篇(1)
运维·服务器·前端·python·html
啃火龙果的兔子27 分钟前
nextjs+react项目如何代理本地请求解决跨域
前端·react.js·前端框架
拾光拾趣录31 分钟前
用Promise打造智能任务队列
前端·javascript·promise
WildBlue34 分钟前
🚀 React Fragment:让代码呼吸的新鲜空气
前端·react.js
遂心_34 分钟前
当 React 遇见数据获取:Ajax 与 Axios 的奇妙冒险 🚀
前端·javascript·react.js
然我35 分钟前
纯函数:相同输入必出相同输出?这才是代码界的 “老实人”
前端·javascript·面试