前端 MVC 分层的实践

目录

前言

并未过时的 MVC

MVC 的由来

不同版本的 MVC

苹果版本

微软版本

阮一峰版

原生 JS 实现 MVC

Model

View

Controller

React 中的 MVC

其他分层

Service

format

utils

目录

总结


前言

前面我们讲了 JavaScript 面向对象编程,这篇文章我们会介绍一下面向对象中的经典编程模式 —— MVC。

MVC、MVVM、MVP 这三个概念在前端领域是老生常谈了,但这节课不会只在概念层面讲述三者的区别,而是更偏向实践、从编写业务代码层面讨论一下 MVC 模式在前端开发中的意思。

并未过时的 MVC

在 Angualr、Vue 等 MVVM 框架出现前,最火的前端框架当属 Backbone,这是一个典型的 MVC 框架。也许你会说 Backbone 不是过时了吗?那还在前端中讨论 MVC 还有什么意义?

Backbone 的确过时了,但是过时是因为 Backbone 的 MVC 实现方式过时了,并非是 MVC 思想过时了。如果以 Vue/React 作为 View 层,Vuex/Redux 作为 Model 层,那么就可以实现新的 MVC 框架。

工业聚大佬在此基础上实现了一个 react-imvc 的框架,也为此写过一篇文章:IMVC(同构 MVC)的前端实践

MVC 分层有助于管理复杂的应用程序,因为你可以在一个时间内只关注于某一层。例如,你可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。

即使是不使用 Angular/React/Vue 这些框架,我们依然能用 MVC 对代码进行组织管理。

MVC 的由来

MVC 是一种架构模式,最早由施乐的 Trygve Reenskaug 在1978年提出,原本是为了给程序语言 Smalltalk 提供架构,大大提高了程序的后期可维护性。

那么什么 MVC 呢?相信大家都对 MVC 比较熟悉了,不管是 Java 中的 spring mvc,还是前端里面的 Backbone,这些都是应用很广泛的 MVC 框架。

我这里引用一下维基百科的解释:

MVC 模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

MVC 模式最早由 Trygve Reenskaug 在 1978 年提出,是施乐帕罗奥多研究中心(Xerox PARC)在 20 世纪 80 年代为程序语言 Smalltalk 发明的一种软件架构。MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:

控制器(Controller):负责转发请求,对请求进行处理。
视图(View):界面设计人员进行图形界面设计。
模型(Model):程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计 (可以实现具体的功能)。


在传统的MVC(Model - View - Controller)架构中,M代表模型(Model),用于表示应用程序的数据结构和业务逻辑。模型通常负责处理数据的存储、检索、更新和删除等操作,以及定义应用程序的业务规则。

然而,在一些现代的前端框架和库中,特别是在React、Vue等组件化框架中,对MVC架构进行了一定程度的演变和扩展。在这些框架中,通常会将数据管理和业务逻辑抽象出来,形成类似于状态管理、数据流管理的概念,而不再严格遵循传统的MVC模式。

因此,在你提供的代码中,虽然使用了MVC的术语,但实际上可能并不是严格意义上的传统MVC架构。在现代的前端开发中,模型(Model)的概念可能会被更广泛地解释为数据管理、状态管理等,而不仅仅局限于数据的存储。因此,即使在前端开发中,也可以将数据管理、状态管理等内容归类为模型(Model)的范畴,这也是为什么在你的代码中可以将数据管理部分称为M的原因。

 


因此,我们可以知道 MVC 是由 Controller、Model和 View 三部分组成,这三部分各司其职。

  1. View: 展示给用户的界面,可以接受用户输入
  2. Model: 存放应用的所有数据,数据来源可能是接口、本地缓存等等
  3. Controller: 负责连接 Model 和 View,从 View 获得输入,修改相关 Model 的数据后,再去通知相关 View 进行更新

不同版本的 MVC

对于 MVC,业界有不同的定义,比如就有微软的版本、苹果的版本等等。

苹果版本

苹果版本的 MVC:

image_1df5q2h5q1t9b156vda41qkqfq19.png-35.4kB

整理一下苹果版本的 MVC,是这样的:

  1. View 发送用户的动作给 Controller
  2. Controller 去更新对应的 Model
  3. Model 改变后会去通知 Controller 更新 View
  4. Controller 收到通知后修改 View

这个版本的 MVC 模式比较符合我们前端开发的直觉,因此本文也以苹果的版本来讲解。

微软版本

微软的版本(图片来自微软 ASP.NET core 官网):

image_1df5q84hg1l1ftc2h1s1mgrsmdm.png-38.8kB

微软的版本以 ASP.NET core MVC 为实现,可以这样理解:

  1. 用户请求被路由到 Controller
  2. Controller 到 Model 执行用户操作和查询结果
  3. Controller 选择要显示给用户的 View
  4. Controller 为 View 提供所需的 Model 数据

可以看得出来,微软版本的 MVC 比较难理解,因此这里不做太多介绍。

阮一峰版

阮一峰的版本:

image_1dfa1ubm37e7gevk03hb917p113.png-31kB

阮一峰版本的 MVC 可以这样理解:

  1. 用户和应用交互
  2. 控制器的事件处理器被触发
  3. 控制器从模型中请求数据,并将其交给视图
  4. 视图将数据呈现给用户

原生 JS 实现 MVC

我们要实现的 MVC 模式如下,类似于 Backbone 的设计模式:

  1. view 会在 model 中进行注册
  2. 用户在页面上进行操作,触发 action 通知 controller
  3. controller 去更新 model 中的数据
  4. controller 通知 view 重新渲染

image_1e3bnr3d015sg1js51ua5u9b5oe9.png-26kB

这里我们要用原生 JS 来实现一个 MVC 模式,在业务代码中也可以使用这样的形式来组织项目。
首先,对于项目来说,我们应该以 app.js 为入口,这是一个业务模块。

Model

model.js 对应 Model,这里定义了应用的数据模型,只做提供数据存储和修改。
在这里可以实现了一个 发布-订阅 功能,在 view 中会订阅当前状态的变化,一旦状态发生变化,就去通知所有订阅的 view 重新渲染。

app.Model = class Model {constructor(todos = []) {this.todos = todos;this.views = [];}findTodoById(id) {// 根据id查找}getAll() {return this.todos;}addTodo(todo) {this.todos.push(todo);}updateTodo(id) {// 更新todo信息}removeTodoById(id) {// 根据id删除}getActiveTodo() {// 拿到未完成的}getCompletedTodo() {// 拿到已完成的}toggleStatusById(id) {}// 这里可以对view做一个注册register(view) {this.views.push(view);}// 对所有监听的view进行通知notify() {for(var i = 0; i < views.length; i++) {this.views[i].render(this);}}
}

View

View.js 对应 View 层,提供数据进行视图渲染,一般是 html 模板文件,也可以是 React/View 等现代化 UI 框架。这里以 underscore template 来举例子。

// 这是underscore的模板语法
<script type="text/template" id="tpl"><ul id='todoList'><% for (var i = 0; i < model.todos.length; i++) {%><li data-id='<%=model.todos[i].id%>' class='<% model.todos[i].isActive ? 'active' : '''%><%=model.todos[i].name%></li><% } %></ul>
</script>

当然,如果你觉得无法理解这个模板语法,你也可以用 es6 模板字符串来实现。

function ToDoList(props) {return `<ul id="todoList">${props.todos.reduce((str, todo) => {return `${str}<li data-id=${todo.id} class=${todo.isActive ? "active" : ""}>${todo.name}</li>`}, '')}</ul>`
}

然后将 model 中的数据传给这个模板来渲染。

// 在render方法中对模板进行重新渲染
app.View = class View {render(model) {const template = _.template($('#tpl').html(), model)$('body').html(template);}
}
// es6 写法
app.View = class View {render(model) {$('body').html(ToDoList(model));}
}

 

Controller

controller.js 对应 Controller,主要是 Model 和 View 之间的纽带,进行一系列事件绑定等功能。
如果你有开发过大型的 jQuery 项目,就会遇到这种问题。当绑定的事件越来越多的时候,在代码里面很难看出来 DOM 和事件之间的绑定关系,维护起来也越来越吃力。
所以我们可以将 DOM 和事件绑定集中在 Controller 中来处理。在 events 属性中,我们将需要绑定的 DOM 元素、事件以及回调函数用键值对的形式进行映射,在 delegateEvents 中解析后进行事件绑定。这样的好处就是可以很清晰地看到项目中所有的绑定事件,后期维护起来更容易。

const events = {"click li": "toggle","submit #form": "submit"
}
// 解析上面的 events 属性
const delegateEvents = () => {// 正则表达式分组捕获var eventSplitter =  /^(\w+)\s*(.*)$/;for (var key in events) {var methodName = events[key];var match = key.match(eventSplitter);// 解析出来的事件名和绑定的 DOMvar eventName = match[1], selector = match[2];// 如果没有要绑定的 DOM,那么绑定在根节点上if (selector === '') {el.bind(eventName, methodName);} else {el.delegate(selector, eventName, methodName);}}

同时,在初始化以及每次操作之后,我们可以选择手动通知(执行 notify 方法)相关视图进行渲染。
最终的代码实现如下:

app.Controller = class Controller {events = {"click li": "toggle"}constructor(el) {this.el = el;}init(view, model) {this.view = view;this.model = model;this.model.register(view);this.model.notify();this.delegateEvents();}// 解析上面的 events 属性delegateEvents() {var eventSplitter: /^(\w+)\s*(.*)$/;for (var key in this.events) {var methodName = this.events[key];var match = key.match(eventSplitter);var eventName = match[1], selector = match[2];if (selector === '') {this.el.bind(eventName, method);} else {this.el.delegate(selector, eventName, method);}}toggle(event) {const id = event.dataset.id;this.view.toggleStatusById(id);this.model.notify();}
}
// app.js
new TodoController('#todoList').init(new app.View()), new app.Model());

这样我们就实现了一个完整的 MVC 骨架。这种形式可以适用于很多项目,甚至不需要依赖框架,后续还可以将 underscore template 替换成 React/Vue 等框架,增加了项目的可维护性,层次结构更加清晰。

React 中的 MVC

其他分层

如果遇到更复杂的业务逻辑,上面的 MVC 分层是远远不够的,部分逻辑可以从 Controller 和 Model 层中剥离出来,也许你还需要下面的这些分层。

Service

Service 层一般是封装了和请求数据相关的操作,比如 Ajax 请求、localStorage、indexDB,甚至是 JS Bridge 等等,这里类似于后端里面的 Model 概念。
比如向后端请求数据我们可以这么来写:

class Service {async getList() {const {data = {}} = await http({method: 'post',url: '/getList',data: {}});return data.list;}
}

在需要用到这个接口的地方(一般是 Controller 层)导入对应的 Service 文件,将接口请求的操作放到 Service 中统一处理,方便后期维护。

format

在将数据请求回来后,往往会出现接口的数据和前端要展示的数据结构不一致的情况。这个时候,从 api -> model 就需要一层额外的转化,这就是 format 层存在的意义。
理论上,format 函数全都应该是纯函数,接收从接口获取的数据,返回需要传给 model 的数据。
下面举个例子,这是一个将接口返回的时间戳转换为 YYYY-MM-DD 格式日期的 format 函数。

const formatDate = (datespan) => {let date = new Date(datespan),year = date.getFullYear(),month = date.getMonth() ,day = date.getDate();const addPrefix = (num) => {return num > 9 ? num : `0${num}`;}month = addPrefix(month + 1);day = addPrefix(day);return `${year}-${month}-${day}`
}
formatDate(1572781090844); // "2019-11-03"

utils

除了数据请求和格式化之外,往往项目中也会出现一些共用的工具函数,比如日期格式化、埋点、DOM 相关的方法等等。

目录

因此,一个完整的项目目录结构应当是这样的(加号代表文件夹,减号代表文件):

+ project+ pages+ home+ css- index.scss- model.js- view.js- controller.js- app.js+ components+ share+ utils+ service+ formatter

service 到底是放到每个页面下面维护还是放到全局维护,这个主要看你的接口是否会经常在多个页面使用,这样的话更推荐你放到全局。
最后,一个完整的流程应当是这样,Controller 层调用 Service 层的方法去获取数据,将拿到的数据传给 format 和 utils 等函数进行转换,最后将转换的数据存入到 Model 中。

image_1dooi085a3afmg7175ab1u8du9.png-17.7kB

总结

虽历经几十年的洗礼,MVC 架构依然没有过时,在各种语言中我们也经常能看到对应的 MVC 框架。
在前端开发中,我们也可以借鉴 MVC 的思想来对项目进行重新组织和解耦,这样可以大大提高项目的灵活性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/13928.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习——图像分类(CNN)—训练模型

训练模型 1.导入必要的库2.定义超参数3.读取训练和测试标签CSV文件4.确保标签是字符串类型5.显示两个数据框的前几行以了解它们的结构6.定义图像处理参数7.创建图像数据生成器8.设置目录路径9.创建训练和验证数据生成器10.构建模型11.编译模型12.训练模型并收集历史13.绘制损失…

【Text2SQL 经典模型】SQLNet

论文&#xff1a;SQLNet: Generating Structured Queries From Natural Language Without Reinforcement Learning ⭐⭐⭐⭐ Code: SQLNet | paperwithcodeSQLNet| GitHub 一、论文速读 这篇论文强调了一个问题&#xff1a;order-matters problem —— 意思是说&#xff0c;对…

2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(中):订阅智能体OSS实现

传送门&#xff1a; 《2024.5组队学习——MetaGPT&#xff08;0.8.1&#xff09;智能体理论与实战&#xff08;上&#xff09;&#xff1a;MetaGPT安装、单智能体开发》《2024.5组队学习——MetaGPT&#xff08;0.8.1&#xff09;智能体理论与实战&#xff08;下&#xff09;&…

【线段图案】

描述 KiKi学习了循环&#xff0c;BoBo老师给他出了一系列打印图案的练习&#xff0c;该任务是打印用“*”组成的线段图案。 输入描述&#xff1a; 多组输入&#xff0c;一个整数&#xff08;1~100&#xff09;&#xff0c;表示线段长度&#xff0c;即“*”的数量。 输出描述…

是德科技 DSOS054A MSOS054A示波器

产品 带宽 通道数 最大存储器深度 DSOS054A 高清晰度示波器 500 MHz 4 个模拟通道 800 Mpts MSOS054A 高清晰度示波器 500 MHz 4 个模拟通道和 16 个数字通道 800 Mpts Infiniium S 系列示波…

R语言使用 ggscidca包优雅的绘制支持向量机决策曲线

DCA(Decision Curve Analysis)临床决策曲线是一种用于评价诊断模型诊断准确性的方法&#xff0c;在2006年由AndrewVickers博士创建&#xff0c;我们通常判断一个疾病喜欢使用ROC曲线的AUC值来判定模型的准确性&#xff0c;但ROC曲线通常是通过特异度和敏感度来评价&#xff0c;…

vue项目报错:internal/modules/cjs/loader.js:892 throw err;

前言&#xff1a; vue项目中无法正常使用git&#xff0c;并报错情况。 报错信息&#xff1a; internal/modules/cjs/loader.js:892throw err;^ Error: Cannot find module D:\project\sd_wh_yth_front\node_modules\yorkie\src\runner.js 报错处理&#xff1a; npm install y…

夏天晚上热,早上凉怎么办?

温差太大容易引起感冒 1.定个大概3点的闹钟&#xff0c;起来盖被子。有些土豪可以开空调&#xff0c;我这个咸鱼没有空调。 2.空调调到合适的温度&#xff0c;比如20几度。

【HarmonyOS4学习笔记】《HarmonyOS4+NEXT星河版入门到企业级实战教程》课程学习笔记(十一)

课程地址&#xff1a; 黑马程序员HarmonyOS4NEXT星河版入门到企业级实战教程&#xff0c;一套精通鸿蒙应用开发 &#xff08;本篇笔记对应课程第 18 节&#xff09; P18《17.ArkUI-状态管理Observed 和 ObjectLink》 第一件事&#xff1a;嵌套对象的类型上加上 Observed 装饰器…

基于网络爬虫技术的网络新闻分析(四)

目录 4.2 系统异常处理 4.2.1 爬虫异常总体概况 4.2.2 爬虫访问网页被拒绝 5 软件测试 5.1 白盒测试 5.1.1 爬虫系统测试结果 5.1.2 中文分词系统测试结果 5.1.3 中文文章相似度匹配系统测试结果 5.1.4 相似新闻趋势展示系统测试结果 5.2 黑盒测试 5.2.1 爬虫系统测…

2024电工杯数学建模 - 案例:最短时间生产计划安排

# 前言 2024电工杯(中国电机工程学会杯)数学建模思路解析 最新思路更新(看最新发布的文章即可): https://blog.csdn.net/dc_sinor/article/details/138726153 最短时间生产计划模型 该模型出现在好几个竞赛赛题上&#xff0c;预测2022今年国赛也会与该模型相关。 1 模型描…

CoShNet:使用复数改进神经网络

使用复数改进神经网络 文章目录 一、说明二、了解卷积神经网络三、进入混合神经网络四、令人惊叹的 CoSh 网络五、复杂函数的神奇性质六、相位一致性七、结论 一、说明 本文题为“CoShNet&#xff1a;使用Shearlets的混合复杂值神经网络”&#xff0c;提出了在混合神经网络中使…

深入理解SVM和浅层机器学习算法的训练机制

深入理解SVM和浅层机器学习算法的训练机制支持向量机&#xff08;SVM&#xff09;的训练过程SVM的基本概念SVM的损失函数训练方法 浅层机器学习算法的训练机制决策树K-最近邻&#xff08;K-NN&#xff09;朴素贝叶斯 结论 深入理解SVM和浅层机器学习算法的训练机制 在探讨浅层…

展现金融科技前沿力量,ATFX于哥伦比亚金融博览会绽放光彩

不到半个月的时间里&#xff0c;高光时刻再度降临ATFX。而这一次&#xff0c;是ATFX不曾拥有的桂冠—“全球最佳在线经纪商”(Best Global Online Broker)。2024年5月15日至16日&#xff0c;拉丁美洲首屈一指的金融盛会—2024年哥伦比亚金融博览会(Money Expo Colombia 2024) 于…

AI智能体|使用扣子Coze基于IDE创建自定义插件

大家好&#xff0c;我是无界生长。 在使用Coze的过程中&#xff0c;有些个性化场景无法通过插件商店已有的插件满足&#xff0c;这个时候就需要通过自定义插件的方式来实现业务需求。下面将通过一个实际案例来简单介绍下如何使用Coze基于IDE创建自定义插件&#xff0c;完成在Co…

2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版

简介&#xff1a; 2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版 下载地址 https://www.kuaiyuanya.com/product/article/index/id/33.html 图片&#xff1a;

使用 Django Rest Framework 构建强大的 Web API

文章目录 安装 Django Rest Framework创建序列化器创建视图和 URL 路由配置认证和权限测试 API Django Rest Framework&#xff08;DRF&#xff09;是一个强大的工具&#xff0c;用于在 Django Web 框架中构建灵活且功能丰富的 Web API。它提供了许多功能&#xff0c;包括序列化…

(六)DockerCompose安装与配置

DockerCompose简介 Compose 项目是 Docker 官方的开源项目&#xff0c;负责实现对 Docker 容器集群的快速编排。使用前面介绍的Dockerfile我们很容易定义一个单独的应用容器。然而在日常开发工作中&#xff0c;经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现…

protobuf学习

学习了下protobuf这个工具&#xff0c;可以用来序列化数据结构&#xff0c;而且效率很高&#xff0c;数据可以压缩的更小。 记录下&#xff0c;我这里主要在C#里使用&#xff0c;从NuGet程序包安装以下两个 安装好后可以在该程序目录找到 packages\Google.Protobuf.Tools.3.26.…

在windows中使用wsl下的unbuntu环境

1 unbuntu下载编译环境 编译环境安装命令&#xff1a; sudo apt install gdb sudo apt install gcc sudo apt install g 2 使用vscode正常打开项目&#xff0c;在window中打开的项目&#xff08;官方推荐将项目放在linux中的home目录&#xff09; 但在windows中也可以使用&a…