从零实现简易MVC

我心飞翔 分类:javascript

MVC是什么?

MVC是一种架构设计模式,它的核心是关注点的分离,即将模型、视图、控制器分离开来。

M模型、V视图、C控制器

  • 模型model封装了与业务逻辑相关的数据以及对数据的处理方法,即负责操作所有数据
  • 视图view是它在屏幕上的表示,描绘的是model的当前状态。即负责所有 UI 界面
  • 控制器controller定义用户界面对用户输入的响应方式,起到不同层面间的组织作用,用于控制应用程序的流程,它处理用户的行为和数据model上的改变。即负责M、V之外的其他事情

image

经典MVC模型图

实现一个简易MVC

为了更好地理解MVC模式,我们来用js实现一个简易的mvc。创建一个文件夹,目录结构如下:

        mvc-demo
          |-- index.html
          |-- main.js 
          |-- app.js
          |-- src
              |-- model.js
              |-- view.js
              |-- controller.js
 

使用 npm init -y 命令初始化项目,此命令需要已安装nodejs。

M(modle层)

在src/mvc/model.js中定义一个模型类,该类拥有增删改查数据的方法,并通过new实例时初始化数据。

class Model {
    // 数据模型初始化:绑定数据
    constructor(data) {
        this._data=data
    }
    // 提供数据增删改查操作
    create () { }
    delete () { }
    update (data) {this._data=data}
    get () { return this._data }
}
export default Model
 

V(view层)

在src/mvc/view.js中定义一个视图类,提供视图更新操作,new实例时初始化页面设置(绑定页面元素和html模板)

class View {
    // 视图初始化
    constructor(el,template) { 
        this.el = document.querySelector(el)
        this.template = template
    }
    render () { } // 渲染视图
}
export default View
 

C(controller层)

在src/mvc/controller.js中定义一个控制器类,将上述的M、V层组织起来,进行事件绑定、视图更新渲染,处理相关逻辑。


class Controller{
    constructor(model, view, methods) {
        this.model = model
        this.view = view
        this.methods=methods
        this.init()
        this.bindMethods()
    }
    // 初始化方法
    init () {
        // 实现视图渲染方法
        this.view.render = () => { this.view.el.innerHTML = this.view.template.replace('{{data}}',this.model.get()) }
        this.view.render() //渲染视图
    }
    // 绑定按钮事件
    bindMethods () {
        [].forEach.call(this.methods, element => {
            // 采用事件委托的方式绑定事件到容器元素上,防止视图重新渲染时子元素被替换导致监听事件失效
            this.view.el.addEventListener('click',  (e) => {
                if (e.target.id === element.DomID)
                {
                    element.fn() // 执行对应方法
                    this.view.render() // 重新渲染视图
                }
            })
        })
    }
}
export default Controller
 

使用这个简易的MVC

简单起见,我们实现一个四则运算计算器,页面上只提供数据和四个加减乘除操作按钮。
src/index.html内容如下

<body>
    <div id="app"></div>
    <script src='./main.js'></script>
</body>
 

在src/main.js中导入这几个类,写好业务逻辑和相关配置,实现他们。

import Model from "./mvc/model"
import View from "./mvc/View"
import Controller from "./mvc/controller"
const options = {
    data100,
    el:'#app',
    template`<div>{{data}}<div>
    <div><button id="add">+1</button></div>
    <div><button id="sub">-1</button></div>
    <div><button id="mul">x10</button></div>
    <div><button id="div">/10</button></div>
    `,
    methods: [
        {
            DomID'add',
            fn() => { model.update(model.get()+1) }
        },
        {
            DomID'sub',
            fn() => { model.update(model.get()-1) }
        },
        {
            DomID'mul',
            fn() => { model.update(model.get()*10) }
        },
        {
            DomID'div',
            fn:  () => { model.update(model.get()/10) }
        }
    ]
}
const { data, el, template, methods }=options
const model = new Model(data)
const view = new View(el, template)
const controller = new Controller(model, view, methods)

试试效果

1616485741835.gif

优化封装

我们已经成功地实现了一个简易mvc框架,并成功使用它制作了一个小demo,但这个框架还有诸多可以改善的地方。比如代码封装不够简洁,如果要再写一个demo,又得重新写一大串代码,能不能让我只配置相关的数据和方法就好呢?接下来我们开始优化。

再加一层?

软件工程中有这么一句话:没有什么是加一层解决不了的。接下来我们加一层app.js来实现一个mvc具体实现代码的封装,依据最小知识原则,我们在使用app.js创建项目时应该只需要传最少的配置,因此我们来实现一个app实现类。
在src/app.js中实现App类,该类通过构造函数传入配置参数,自动帮我们实例化了mvc每个类的操作,使得我们调用这个类时只需要传一个options参数即可。

import Model from "./mvc/model"
import View from "./mvc/View"
import Controller from "./mvc/controller"
class App{
    constructor({ data, el, template, methods }) {
        this.model = new Model(data)
        this.view = new View(el, template)
        this.controller = new Controller(this.model, this.view, methods)
    }
}
export default App

在src/main.js中引入app.js并配置相关数据参数。由于此时的methods已经无法直接调用model类,因此写成function函数,用this代替,并在controller添加监听事件时绑定this到model。

import App from './app'
new App({
    data100,
    el:'#app',
    template`<div>{{data}}<div>
    <div><button id="add">+1</button></div>
    <div><button id="sub">-1</button></div>
    <div><button id="mul">x10</button></div>
    <div><button id="div">/10</button></div>
    `,
    methods: [
        {
            DomID'add',
            fnfunction (this.model.update(this.model.get()+1) }
        },
        {
            DomID'sub',
            fnfunction (this.model.update(this.model.get()-1) }
        },
        {
            DomID'mul',
            fnfunction (this.model.update(this.model.get()*10) }
        },
        {
            DomID'div',
            fnfunction (this.model.update(this.model.get()/10) }
        }
    ]
})

controller.js修改内容:

    // 绑定按钮事件
    bindMethods () {
        [].forEach.call(this.methods, element => {
            // 采用事件委托的方式绑定事件到容器元素上,防止视图重新渲染时子元素被替换导致监听事件失效
            this.view.el.addEventListener('click',  (e) => {
                if (e.target.id === element.DomID)
                {
                    element.fn.call(this,arguments// 执行对应方法
                    this.view.render() // 重新渲染视图
                }
            })
        })
    }

之后如果想再写一个新的demo,就可以直接复用app.js,简单的传入一些参数:容器元素、数据、绑定事件,就搞定啦,是不是很简洁?

有点像Vue?

app.js封装之后,引入app并传配置的步骤怎么越看越像Vue呢?其实,Vue的MVVM思想就是在MVC的基础之上发展而来的。我们先来看一下MVVM的特点。

M(Model模型) MVVM中的M保存的是每个页面中单独的数据

VM(ViewModel) 它是一个调度者,分割了M和V每当V想要获取后面保存数据的时候,都要由VM做中间的处理

V(view视图) 就是每个页面中的HTML结构

VM是MVVM的思想核心,是 M 和 V 之间的调度者,数据的双向绑定是由 VM 提供的;
MVVM与MVC最大的区别就是:MVVM实现了View和Model的自动同步,不用再自己手动操作Dom元素了,即Model变化时View可以实时更新,View变化也能让Model改变。
因此理解了MVC,我们就理解了Vue的设计思路,因为他们本质上都是一样的,都是通过分离关注点的设计,将代码抽象隔离开来。

MVC、MVVM傻傻分不清楚?

MVVM的出现是为了解决MVC模式的controller层的缺点。MVC诞生的年代,手机app、网页等中的数据比较简单,没有现在这么复杂,因此那时候的数据解析处理很可能一步就解决了,MVC模式中的controller可以很轻松的胜任这些数据处理业务,但随着信息技术的发展,数据越来越庞大,数据处理的业务越来越复杂,此时的Controller显得十分臃肿。于是开发者们创建了一个新的类:ViewModel,专门用来做数据处理,这就是MVVM的由来。

总结

这次我们学习了什么是MVC,并通过自己实现一个简易的mvc框架让我们对MVC的设计思想有了更深的理解。MVC的思想核心在于,它将每个软件程序进行了分层(数据、视图、控制),这个模式的好处就是不论多么复杂的程序,从结构上来看都能将其在结构上分层,三层之间紧密联系,却又相互独立,每一层的变化都不会影响其他层,极大地提升了程序的可维护性和可扩展性。

项目源码地址:github.com/channef/sim…

终端指令:

npm install

npm install parcel-bundler --save-dev

npm run build

回复

我来回复
  • 暂无回复内容