MVP架构模式:被动视图的魅力

MVP架构模式:被动视图的魅力 MVP架构模式被动视图的魅力前言各位前端小伙伴们上一篇我们聊了MVC架构模式今天咱们来聊聊它的一个重要变体——MVPModel-View-Presenter模式。MVP在MVC的基础上做了一些改进让视图变得更加被动从而提高代码的可测试性和可维护性。一、什么是MVPMVP是Model-View-Presenter的缩写是MVC模式的一个变体。它将应用程序分为三个主要部分1.1 MVP三部分职责组件职责说明Model数据层管理应用程序的数据和业务逻辑View视图层被动展示数据不包含业务逻辑Presenter表示层处理用户交互协调Model和View1.2 MVP与MVC的区别特性MVCMVPView角色主动包含部分逻辑被动仅负责展示Controller/Presenter协调者完全掌控者数据绑定直接或间接通过Presenter可测试性中等高View独立性较低较高1.3 MVP工作流程用户交互 → Presenter → Model → 数据更新 → Presenter → View → 视图更新 ↑ | └─────────────────────────────────────────────────────────────────┘二、MVP架构实战2.1 Model层实现// models/UserModel.js class UserModel { constructor() { this.users []; this.nextId 1; this.listeners []; } addListener(listener) { this.listeners.push(listener); } removeListener(listener) { const index this.listeners.indexOf(listener); if (index ! -1) { this.listeners.splice(index, 1); } } notify(event, data) { this.listeners.forEach(listener listener(event, data)); } getAllUsers() { return [...this.users]; } getUserById(id) { return this.users.find(user user.id id); } addUser(userData) { const user { id: this.nextId, ...userData, createdAt: new Date() }; this.users.push(user); this.notify(userAdded, user); return user; } updateUser(id, updates) { const index this.users.findIndex(user user.id id); if (index ! -1) { this.users[index] { ...this.users[index], ...updates }; this.notify(userUpdated, this.users[index]); return this.users[index]; } return null; } deleteUser(id) { const index this.users.findIndex(user user.id id); if (index ! -1) { const deleted this.users.splice(index, 1)[0]; this.notify(userDeleted, deleted); return deleted; } return null; } } export { UserModel };2.2 View层实现// views/UserView.js class UserView { constructor() { this.container document.getElementById(user-container); this.form document.getElementById(user-form); this.nameInput document.getElementById(name); this.emailInput document.getElementById(email); this.onAddUser null; this.onEditUser null; this.onDeleteUser null; this.bindEvents(); } bindEvents() { this.form.addEventListener(submit, (e) { e.preventDefault(); if (typeof this.onAddUser function) { this.onAddUser(this.getFormData()); this.clearForm(); } }); this.container.addEventListener(click, (e) { if (e.target.classList.contains(edit-btn)) { const id parseInt(e.target.dataset.id); if (typeof this.onEditUser function) { this.onEditUser(id); } } else if (e.target.classList.contains(delete-btn)) { const id parseInt(e.target.dataset.id); if (typeof this.onDeleteUser function) { this.onDeleteUser(id); } } }); } renderUsers(users) { this.container.innerHTML users.map(user div classuser-card>// presenters/UserPresenter.js import { UserModel } from ../models/UserModel.js; import { UserView } from ../views/UserView.js; class UserPresenter { constructor(model, view) { this.model model; this.view view; this.bindViewEvents(); this.bindModelEvents(); this.loadUsers(); } bindViewEvents() { this.view.onAddUser this.handleAddUser.bind(this); this.view.onEditUser this.handleEditUser.bind(this); this.view.onDeleteUser this.handleDeleteUser.bind(this); } bindModelEvents() { this.model.addListener((event, data) { switch (event) { case userAdded: this.view.showMessage(用户添加成功); this.loadUsers(); break; case userUpdated: this.view.showMessage(用户更新成功); this.loadUsers(); break; case userDeleted: this.view.showMessage(用户删除成功); this.loadUsers(); break; } }); } async loadUsers() { const users this.model.getAllUsers(); this.view.renderUsers(users); } handleAddUser(userData) { try { if (!userData.name || !userData.email) { throw new Error(请填写完整信息); } this.model.addUser(userData); } catch (error) { this.view.showMessage(添加失败 error.message, error); } } handleEditUser(id) { const user this.model.getUserById(id); if (user) { const updates this.view.showEditDialog(user); if (updates.name updates.email) { this.model.updateUser(id, updates); } } } handleDeleteUser(id) { if (this.view.confirmDelete()) { this.model.deleteUser(id); } } } export { UserPresenter };2.4 应用入口// app.js import { UserModel } from ./models/UserModel.js; import { UserView } from ./views/UserView.js; import { UserPresenter } from ./presenters/UserPresenter.js; document.addEventListener(DOMContentLoaded, () { const model new UserModel(); const view new UserView(); new UserPresenter(model, view); });2.5 HTML结构!DOCTYPE html html head titleMVP Demo/title style .user-card { border: 1px solid #ccc; padding: 16px; margin: 8px; border-radius: 4px; } .message { position: fixed; top: 20px; right: 20px; padding: 16px; border-radius: 4px; color: white; } .message-success { background-color: green; } .message-error { background-color: red; } /style /head body h1用户管理系统/h1 form iduser-form input typetext idname placeholder姓名 required input typeemail idemail placeholder邮箱 required button typesubmit添加用户/button /form div iduser-container/div script typemodule srcapp.js/script /body /html三、MVP的优缺点3.1 优点View完全被动View只负责展示不包含任何业务逻辑高可测试性Presenter可以独立测试不需要DOM环境职责清晰各层职责明确代码结构清晰View可替换可以轻松替换不同的View实现解耦性强Presenter不依赖具体的View实现3.2 缺点复杂度增加比MVC更复杂Presenter可能臃肿所有逻辑都集中在Presenter中学习曲线需要理解被动视图的概念四、MVP在现代框架中的应用4.1 React中的MVP思想// Model const UserStore { users: [], addUser: (user) { UserStore.users.push(user); UserStore.notify(); }, notify: () {} }; // View (被动组件) function UserList({ users, onEdit, onDelete }) { return ( div {users.map(user ( div key{user.id} h3{user.name}/h3 button onClick{() onEdit(user.id)}编辑/button button onClick{() onDelete(user.id)}删除/button /div ))} /div ); } // Presenter (自定义Hook) function useUserPresenter() { const [users, setUsers] useState([]); useEffect(() { setUsers([...UserStore.users]); UserStore.notify () setUsers([...UserStore.users]); }, []); const handleAddUser (user) { UserStore.addUser(user); }; const handleEditUser (id) { // 编辑逻辑 }; const handleDeleteUser (id) { // 删除逻辑 }; return { users, handleAddUser, handleEditUser, handleDeleteUser }; } // 使用 function App() { const { users, handleAddUser, handleEditUser, handleDeleteUser } useUserPresenter(); return ( div UserList users{users} onEdit{handleEditUser} onDelete{handleDeleteUser} / UserForm onSubmit{handleAddUser} / /div ); }4.2 Vue中的MVP思想// Model (Vue Store) const userStore { state: { users: [] }, mutations: { ADD_USER(state, user) { state.users.push(user); }, DELETE_USER(state, id) { state.users state.users.filter(u u.id ! id); } }, actions: { addUser({ commit }, user) { commit(ADD_USER, user); }, deleteUser({ commit }, id) { commit(DELETE_USER, id); } } }; // View (被动组件) const UserList { props: [users], emits: [edit, delete], template: div div v-foruser in users :keyuser.id h3{{ user.name }}/h3 button click$emit(edit, user.id)编辑/button button click$emit(delete, user.id)删除/button /div /div }; // Presenter (父组件) const UserManager { components: { UserList }, computed: { users() { return this.$store.state.users; } }, methods: { handleEdit(id) { /* 编辑逻辑 */ }, handleDelete(id) { this.$store.dispatch(deleteUser, id); } }, template: UserList :usersusers edithandleEdit deletehandleDelete / };五、MVP最佳实践5.1 View保持被动View应该只负责展示和用户交互的触发不包含任何业务逻辑// 好的做法View只负责展示 class GoodView { render(data) { // 只渲染数据 this.container.innerHTML data.map(item div${item}/div).join(); } // 通过回调暴露事件 onButtonClick(callback) { this.button.addEventListener(click, callback); } } // 坏的做法View包含业务逻辑 class BadView { render(data) { // 不应该在这里处理业务逻辑 const processed data.filter(item item.active); // ❌ this.container.innerHTML processed.map(item div${item}/div).join(); } }5.2 Presenter集中处理逻辑所有业务逻辑应该集中在Presenter中class UserPresenter { constructor(model, view) { this.model model; this.view view; this.bindEvents(); } bindEvents() { // 所有事件处理都在这里 this.view.onAddUser this.handleAddUser.bind(this); this.view.onEditUser this.handleEditUser.bind(this); this.view.onDeleteUser this.handleDeleteUser.bind(this); } handleAddUser(data) { // 验证、处理、调用Model if (this.validate(data)) { this.model.addUser(data); } } validate(data) { // 所有验证逻辑集中在这里 return data.name data.email; } }5.3 使用依赖注入通过依赖注入提高可测试性// 便于测试的设计 class UserPresenter { constructor(model, view, validator) { this.model model; this.view view; this.validator validator; } handleAddUser(data) { if (this.validator.validate(data)) { this.model.addUser(data); } } } // 测试时可以注入mock const mockModel { addUser: jest.fn() }; const mockView { onAddUser: null }; const mockValidator { validate: () true }; const presenter new UserPresenter(mockModel, mockView, mockValidator);5.4 事件驱动通信使用事件机制降低耦合class EventBus { constructor() { this.events {}; } subscribe(event, handler) { if (!this.events[event]) this.events[event] []; this.events[event].push(handler); } publish(event, data) { if (this.events[event]) { this.events[event].forEach(handler handler(data)); } } } // 使用 const bus new EventBus(); // Presenter订阅事件 bus.subscribe(userAdded, (user) { presenter.updateView(); }); // Model发布事件 bus.publish(userAdded, user);六、MVP与MVVM对比特性MVPMVVMView角色被动视图数据绑定视图数据更新通过Presenter通过双向绑定复杂度较低较高学习曲线较平缓较陡峭适用场景中小型应用大型复杂应用七、总结MVP架构模式是MVC的一个重要变体Model负责数据和业务逻辑View被动展示不包含业务逻辑Presenter处理所有逻辑协调Model和ViewMVP的优点View完全被动易于测试和替换职责清晰代码结构清晰解耦性强各层独立MVP的缺点复杂度较高Presenter可能变得臃肿MVP适合需要高度可测试性和可维护性的项目。理解MVP有助于我们更好地设计和组织代码。好了今天的分享就到这里。希望大家对MVP架构模式有了更深入的理解最后留个问题给大家你觉得MVP和MVC哪个更适合大型项目为什么欢迎在评论区分享