参考: 自制前端框架之 MVC 参考: MVC,MVP 和 MVVM 的图示
如何设计一个程序的结构,这是一门专门的学问,叫做”架构模式”(architectural pattern),属于编程的方法论。MVC 模式就是架构模式的一种,在 UI 编程领域大有大量使用 MVC 模式的开发框架 (Django 等后端框架),使得开发者能够借助该模式,构建出更易于扩展和维护的应用程序。
MVC 简介 大体上可将 MVC 模式的结构分为三层,即 Model(模型)、View(视图)和 Controller(控制)
Model : 专注数据的存取,对基础数据对象的封装。
Controller : 处理具体业务逻辑,即根据用户从”视图层”输入的指令,从 Model 中存取数据,并渲染到 View 中。
View : 负责界面视图,供用户查看和操作,可理解为【输入数据,输出界面】的模块,在其中通常不涉及的业务逻辑。
这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供其他层调用。这样一来,软件就可以实现模块化,修改外观或者变更数据都不用修改其他层,大大方便了维护和升级。
需要注意的是,MVC 仅是一种模式理念,而非具体的规范。因此,根据 MVC 的理念所设计出的框架,在实现和使用上可能存在着较大的区别。
咱们的目标 常见的后端框架所封装的功能,不外乎对数据的增查改删与渲染。在前端,我们以一个非常简单的 Todo App 作为示例,来实际看看 MVC 模式到底是怎样工作的。
Model 模块实现 Todo 这一数据模型的存取。
View 模块实现将 Todo 数据模型渲染到页面。
Controller 模块实现对 Todo 数据的新增、编辑、删除等操作。
编写代码 Model 模块 按照 MVC 模式,Model 模块的主要工作是存取数据,并且在数据变化时将新数据传给 View 模块 。Model 模块的核心是订阅-发布者模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Model { constructor ( ) { this .todo = []; this .todo ; this .subscribers = []; } get data () { return this .todo ; } set data (data ) { this .todo = data; this .publish (this .todo ); } publish (data ) { this .subscribers .forEach (render => render (data)); } }
在示例中可以发现,所谓的发布 - 订阅模式,其思路和实现均非常简单:
区分出【发布者】和【订阅者】的概念。本例中 Model 为发布者,Controller 为订阅者。
在发布者中维护【我有哪些订阅者】信息的数组,每个元素为一个订阅者提供的回调。
发布者数据更新时,依次触发所有订阅者的回调。
不过,Model 中的代码仅实现了【初始化发布者】与【触发所有订阅】,【数据更新时,触发订阅回调】的功能,并不是一个完整的发布 - 订阅模式。在完整的模式实现中,其余代码包括:
【订阅者订阅发布者】机制的实现,其代码位置为 Controller 中的最后一行 this.model.subscribers.push(this.render),在此将 render 方法作为订阅者回调,提供给了发布者。
【订阅者提供的订阅方法】的实现,在此即为 Controller 中提供的 this.render 方法。
Controller 模块 上文中已经明确,Controller 模块需要实现的功能为:
与 Model / View 实例的绑定。
对点击事件、DOM 选择等底层 API 的封装。
用于渲染数据的 Render 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Controller { constructor (conf ) { this .el = document .querySelector (conf.el ); this .model = conf.model ; this .view = conf.view ; this .bindEvent (this .el ); this .render = this .render .bind (this ); this .model .subscribers .push (this .render ); } bindEvent ( ) { this .el .addEventListener ('click' , event => { let el = event.target ; let id = el.dataset .id ; if (el.classList .contains ('todo-delete' )) { deleteTodo (id); } else if (el.classList .contains ('todo-update' )) { updateTodo (el, id); } else if (el.classList .contains ('todo-add' )) { addTodo (el); } }); const addTodo = el => { let input = el.parentElement .querySelector ('.input-add' ); let value = input.value ; if (value !== '' ) { let data = this .model .data ; data.push ({ id : data.length , value : value, }); this .model .data = data; } }; const deleteTodo = id => { this .model .data = this .model .data .filter (todo => { return id !== String (todo.id ); }); }; const updateTodo = (el, id ) => { this .model .data = this .model .data .map (todo => { return { id : todo.id , value : setValue (id, todo), }; }); }; const setValue = (id, todo ) => { if (id === String (todo.id )) { let updateInput = document .querySelector (`input[data-id="${todo.id} "]` ); return updateInput.value ; } else { return todo.value ; } }; } render ( ) { this .el .innerHTML = this .view (this .model .todo ); } }
可以看到 Controller 模块在点击事件触发时,没有直接修改 dom, 只是修改了 model 中的数据, dom 视图的改变是在 model 中数据变化时自动 render 的。
View 模块 如前文所述, View 模块实质上就是一个在 Model 中数据变更时,由 Controller 在 render 方法中执行的一个纯函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function view (todos ) { const todosList = todos .map ( todo => ` <div> <span data-id="${todo.id} "> ${todo.value} </span> <button data-id="${todo.id} " class="todo-delete"> 删除 </button> <span> <input data-id="${todo.id} "/> <button data-id="${todo.id} " class="todo-update"> Update </button> </span> </div> ` ) .join ('' ); return ` <main> <input class="input-add"/> <button class="todo-add">Add</button> <div>${todosList} </div> </main> ` ;}
index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > Document</title > </head > <body > <div id ="app" > </div > <script src ="./mvc.js" > </script > <script > const model = new Model (); let conf = { model, view, el : '#app' , }; const controller = new Controller (conf); controller.render (); </script > </body > </html >
总结 在实现 MVC 模式的 todo app 的过程中,MVC 模式的特性得到了体现,【Model, View】模块直接只对外提供接口, 【Controller】模块则将两者连接了起来,而 ES6 所提供的 class 高级特性则大大简化这些特性的实现复杂度(setter getter 等)。
最后的最后,咱们再梳理下流程:
点击按钮 -> 触发 controller 里的事件 -> 更改 model 数据 -> 触发 render 函数 -> 更新视图