前端 MVC 分层的实践

news2024/11/24 19:59:55

目录

前言

并未过时的 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);
        // 解析出来的事件名和绑定的 DOM
        var 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.coloradmin.cn/o/1689552.html

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

相关文章

图书管理系统(Java版本)

文章目录 前言要求1.设置对象1.1.图书1.2.书架2.管理员3.功能的实现 2.搭建框架2.1.登录(login)2.2.菜单2.3.操作方法的获取 3.操作方法的实现3.1.退出系统(ExitOperation)3.2.显示图书(ShowOperation)3.3.查阅图书(FindOperation)3.4.新增图书(AddOperation)3.5.借出图书(Borr…

go-zero 实战(5)

引入Prometheus 用 Prometheus 监控应用 1. 用 docker 启动 Prometheus 编辑配置位置&#xff0c;我将 prometheus.yaml 和 targets.json 文件放在了 /opt/prometheus/conf目录下 prometheus.yaml global:scrape_interval: 15s # 抓取间隔evaluation_interval: 15s # 评估…

宝塔Linux下安装EMQX服务并设置匿名访问

简述 之前有在Windows和Linux下搭建过EMQX服务并且使用方面都没问题,但那都是使用的用户和密码方式访问,且前提都是通过浏览器进入EMQX的配置页面设置的属性; 但这次使用的是腾讯云租用的宝塔Liniux,由于没有浏览器只能通过命令行方式修改EMQX配置以达到目的;由于事先没看…

Android studio关闭自动更新

Windows下&#xff1a; 左上角file - setting - Appearance & Behavier - system setting - update - 取消勾选

【实战】SpringBoot整合Websocket、Redis实现Websocket集群负载均衡

文章目录 前言技术积累什么是Websocket什么是Redis发布订阅Redis发布订阅与消息队列的区别 实战演示SpringBoot整合WebsoketWebsoket集群负载均衡 实战测试IDEA启动两台服务端配置nginx负载均衡浏览器访问模拟对话 前言 相信很多同学都用过websocket来实现服务端主动向客户端推…

案例题(第二版)

案例题目 信息系统架构设计 基本概念 信息系统架构&#xff08;ISA&#xff09;是对某一特定内容里的信息进行统筹、规划、设计、安排等一系列的有机处理的活动。特点如下 架构是对系统的抽象&#xff0c;它通过描述元素、元素的外部可见属性及元素之间的关系来反映这种抽象…

初识C语言——第二十八天

代码练习1&#xff1a; 用函数的方式实现9*9乘法表 void print_table(int n) {int i 0;int j 0;for (i 1; i< n; i){for (j 1; j< i; j){printf("%d*%d%-3d ", i, j, i * j);}printf("\n");}}int main() {int n 0;scanf("%d", &a…

2024-5-24 石群电路-15

2024-5-24&#xff0c;星期五&#xff0c;22:15&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天最后一天上班&#xff0c;终于要放返校假啦&#xff0c;开心&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;不过放假也不能耽误…

day16|二叉树的属性

相关题目 ● 104.二叉树的最大深度 559.n叉树的最大深度 ● 111.二叉树的最小深度 ● 222.完全二叉树的节点个数 二叉树的深度与高度 如图&#xff0c; 二叉树的深度表示&#xff1a;任意一个叶子节点到根节点的距离&#xff0c;是从上往下计数的&#xff0c;因此使用前序遍历…

忍の摸头之术游戏娱乐源码

本资源提供给大家学习及参考研究借鉴美工之用&#xff0c;请勿用于商业和非法用途&#xff0c;无任何技术支持&#xff01; 忍の摸头之术游戏娱乐源码&#xff0c;抖音上面非常火的摸头杀画面,看得我眼花缭乱,源码拿去玩吧&#xff1b; 目录说明 忍の摸头之术&#xff1a;域…

[牛客网]——C语言刷题day5

答案&#xff1a;D 解析&#xff1a;因为两个指针都指向的字符串常量&#xff0c;不能被重新赋值&#xff0c;*p*q是错误的 在C语言中&#xff0c;赋值语句的返回值都是所赋的值&#xff0c;所以才会有连续赋值的语句&#xff0c;例如ab10&#xff0c;因此&#xff0c;这里的i…

TypeScript-初识

TypeScript 是具有类型语法的JavaScript&#xff0c;是一门强类型的编程语言 变量不能做随意类型赋值 好处&#xff1a; 1️⃣ 静态类型检查&#xff0c;提前发现代码错误 function arrToStr(arr: Array<string>){return arr.join() } arrToStr(123) // 类型“stri…

汇聚荣科技有限公司优点有哪些?

在当今快速发展的科技时代&#xff0c;企业之间的竞争愈发激烈。作为一家专注于科技创新与研发的公司&#xff0c;汇聚荣科技有限公司凭借其卓越的技术实力和创新能力&#xff0c;在业界树立了良好的口碑。那么&#xff0c;汇聚荣科技有限公司究竟有哪些优点呢?接下来&#xf…

基于CentOS7的openGauss5.x极简版安装过程分享

背景&#xff1a;国产信创适配大环境下&#xff0c;安装并体验一下&#xff0c;了解一些数据库适配情况 约束&#xff1a;CentOS Linux release 7.8.2003 (Core) 范围&#xff1a;仅记录上述平台下的简单安装体验过程 目的&#xff1a;节约大家初次体验的时间&#xff0c;为社会…

Python协程的作用

过分揣测别人的想法&#xff0c;就会失去自己的立场。大家好&#xff0c;当代软件开发领域中&#xff0c;异步编程已成为一种不可或缺的技术&#xff0c;用于处理大规模数据处理、高并发网络请求、实时通信等应用场景。而Python协程&#xff08;Coroutine&#xff09;作为一种高…

string类的各个功能函数的底层实现

我们在上一篇文章中详细地讲解了string类的各个常用功能成员函数的讲解&#xff0c;本文我们将对上文进行一个小收尾&#xff0c;然后开始实现string类的底层。 一、上一篇的收尾 1.find函数&#xff1a;顾名思义&#xff0c;它的功能是在字符串中找到目标字符并返回它的位置…

骨传导耳机哪个品牌好?避坑必读精析5大热门款式推荐!

作为一名有着多年工作经验的数码测评师&#xff0c;我发现市场上有许多骨传导耳机品牌都声称自己具有出色的音质和佩戴舒适度。但是&#xff0c;从用户的实际反馈来看&#xff0c;大部分产品都或多或少存在一些问题&#xff0c;如音质失真、佩戴不稳定、耳朵不适等&#xff0c;…

【Pytorch】16.使用ImageFolder加载自定义MNIST数据集训练手写数字识别网络(包含数据集下载)

数据集下载 MINST_PNG_Training在github的项目目录中的datasets中有MNIST的png格式数据集的压缩包 用于训练的神经网络模型 自定义数据集训练 在前文【Pytorch】13.搭建完整的CIFAR10模型我们已经知道了基本搭建神经网络的框架了&#xff0c;但是其中的数据集使用的torchvision…

vue实现加入购物车动效

实现 实现逻辑&#xff1a; 点击添加购物车按钮时&#xff0c;获取当前点击位置event的clientX 、clientY&#xff1b;动态创建移动的小球&#xff0c;动态计算小球需要移动到的位置&#xff08;通过ref 的getBoundingClientRect获取统计元素按钮位置&#xff09;&#xff1b…

JS 网页密码框验证信息

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>/* 当没有密码…