如果遇到更复杂的业务逻辑,上面的 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"
service 到底是放到每个页面下面维护还是放到全局维护,这个主要看你的接口是否会经常在多个页面使用,这样的话更推荐你放到全局。
最后,一个完整的流程应当是这样,Controller 层调用 Service 层的方法去获取数据,将拿到的数据传给 format 和 utils 等函数进行转换,最后将转换的数据存入到 Model 中。
前言
前面我们讲了 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 是由 Controller、Model和 View 三部分组成,这三部分各司其职。
不同版本的 MVC
对于 MVC,业界有不同的定义,比如就有微软的版本、苹果的版本等等。
苹果版本
苹果版本的 MVC:
整理一下苹果版本的 MVC,是这样的:
这个版本的 MVC 模式比较符合我们前端开发的直觉,因此本文也以苹果的版本来讲解。
微软版本
微软的版本(图片来自微软 ASP.NET core 官网):
微软的版本以 ASP.NET core MVC 为实现,可以这样理解:
可以看得出来,微软版本的 MVC 比较难理解,因此这里不做太多介绍。
阮一峰版
阮一峰的版本:
阮一峰版本的 MVC 可以这样理解:
原生 JS 实现 MVC
我们要实现的 MVC 模式如下,类似于 Backbone 的设计模式:
这里我们要用原生 JS 来实现一个 MVC 模式,在业务代码中也可以使用这样的形式来组织项目。 首先,对于项目来说,我们应该以 app.js 为入口,这是一个业务模块。
Model
model.js 对应 Model,这里定义了应用的数据模型,只做提供数据存储和修改。 在这里可以实现了一个 发布-订阅 功能,在 view 中会订阅当前状态的变化,一旦状态发生变化,就去通知所有订阅的 view 重新渲染。
View
View.js 对应 View 层,提供数据进行视图渲染,一般是 html 模板文件,也可以是 React/View 等现代化 UI 框架。这里以 underscore template 来举例子。
当然,如果你觉得无法理解这个模板语法,你也可以用 es6 模板字符串来实现。
然后将 model 中的数据传给这个模板来渲染。
Controller
controller.js 对应 Controller,主要是 Model 和 View 之间的纽带,进行一系列事件绑定等功能。 如果你有开发过大型的 jQuery 项目,就会遇到这种问题。当绑定的事件越来越多的时候,在代码里面很难看出来 DOM 和事件之间的绑定关系,维护起来也越来越吃力。 所以我们可以将 DOM 和事件绑定集中在 Controller 中来处理。在 events 属性中,我们将需要绑定的 DOM 元素、事件以及回调函数用键值对的形式进行映射,在 delegateEvents 中解析后进行事件绑定。这样的好处就是可以很清晰地看到项目中所有的绑定事件,后期维护起来更容易。
同时,在初始化以及每次操作之后,我们可以选择手动通知(执行 notify 方法)相关视图进行渲染。 最终的代码实现如下:
这样我们就实现了一个完整的 MVC 骨架。这种形式可以适用于很多项目,甚至不需要依赖框架,后续还可以将 underscore template 替换成 React/Vue 等框架,增加了项目的可维护性,层次结构更加清晰。
React 中的 MVC
其他分层
如果遇到更复杂的业务逻辑,上面的 MVC 分层是远远不够的,部分逻辑可以从 Controller 和 Model 层中剥离出来,也许你还需要下面的这些分层。
Service
Service 层一般是封装了和请求数据相关的操作,比如 Ajax 请求、localStorage、indexDB,甚至是 JS Bridge 等等,这里类似于后端里面的 Model 概念。 比如向后端请求数据我们可以这么来写:
在需要用到这个接口的地方(一般是 Controller 层)导入对应的 Service 文件,将接口请求的操作放到 Service 中统一处理,方便后期维护。
format
在将数据请求回来后,往往会出现接口的数据和前端要展示的数据结构不一致的情况。这个时候,从
api -> model
就需要一层额外的转化,这就是 format 层存在的意义。 理论上,format 函数全都应该是纯函数,接收从接口获取的数据,返回需要传给 model 的数据。 下面举个例子,这是一个将接口返回的时间戳转换为 YYYY-MM-DD 格式日期的 format 函数。utils
除了数据请求和格式化之外,往往项目中也会出现一些共用的工具函数,比如日期格式化、埋点、DOM 相关的方法等等。
目录
因此,一个完整的项目目录结构应当是这样的(加号代表文件夹,减号代表文件):
service 到底是放到每个页面下面维护还是放到全局维护,这个主要看你的接口是否会经常在多个页面使用,这样的话更推荐你放到全局。 最后,一个完整的流程应当是这样,Controller 层调用 Service 层的方法去获取数据,将拿到的数据传给 format 和 utils 等函数进行转换,最后将转换的数据存入到 Model 中。
总结
虽历经几十年的洗礼,MVC 架构依然没有过时,在各种语言中我们也经常能看到对应的 MVC 框架。 在前端开发中,我们也可以借鉴 MVC 的思想来对项目进行重新组织和解耦,这样可以大大提高项目的灵活性。