如何实现一个MV*模式(MVC/MVP/MVVM)

假如让你不依托任何前端框架(React/Vue/Angular等等),单纯用Javascript编写一个网站应用,你还知道怎么开发吗?

举个例子,产品经理让你实现一个网页,上面有一张猫咪的图片,猫咪的下面显示点赞的次数。每次点击猫咪的图片,点赞的数字加一。

这个对大家来说应该都很简单。

这时候产品经理开始加需求了,网页上展现五张猫咪图片,分别有自己的点赞次数,点击猫咪图片,相对应的点赞次数加一。这时候你想怎么改写自己的程序呢?你的程序现在看起来是否逻辑清楚,结构清晰,可拓展性强呢?

今天我就要带大家用MV模式来组织代码,编写出高质量优美的前端项目。首先我们要大概搞清楚一些什么MV模式。

什么MV*模式

MV*是MVC/MVP/MVVM等的一个统称,它们各有不同,但本质上其实是一个东西。MVP和MVVM是MVC的变体。所以我们今天不谈论它们的区别,只关注核心的东西。

M代表的是Model,用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。Model有对数据直接访问的权力,例如对数据库的访问。Model 不关心它会被如何显示或是如何被操作。

V代表的是View,用于将数据有目的的显示出来,在 View 中一般没有程序上的逻辑。

最后的*,不管是Controller还是Presenter,还是ViewModel,本质上做的事情就是连接M和V,搭建M和V沟通的桥梁。让M和V不直接沟通,达到职责分离的效果。

我们可以看维基百科上一个极简的MVC实现:

/** 模拟 Model, View, Controller */
var M = {}, V = {}, C = {};

/** Model 负责存放资料 */
M.data = "hello world";

/** View 负责将资料显示到屏幕上 */
V.render = (m) => { alert(m.data); }

/** Controller 作为一个 M 和 V 的桥梁 */
C.handleOnload = () => { V.render(M); }

/** 在网页读取的时候调用 Controller */
window.onload = C.handleOnload;

我们今天要实现的MV*就要满足这几个条件:

  1. Model保存我们的数据
  2. View负责渲染节点,可以有多个View
  3. *(我们给它取个名字叫Bridge)为View提供读取和修改Model的方法

产品经理的需求

最终版:网页左侧展示一个可选择的猫咪名字列表,右侧展示当前选中的猫咪详情,包括猫咪名称,猫咪图片,该猫咪被点赞数量和一个点赞按钮。点击点赞按钮,当前猫咪的点赞数量加1。效果图如下,我们只关心功能实现,所以样式丑我们先忍一下。

如何实现一个MV*模式(MVC/MVP/MVVM)_第1张图片

HTML && CSS

    placekitten
    ?

    结构很清楚,主要有一个id是cat-list的猫咪列表和一个id是cat-container的猫咪详情,猫咪详情包括三部分,猫咪名字,猫咪图片和点赞区。添加一点简单的样式。

    #main {
      display: flex;
    }
    
    #cat-list {
      flex: 0 0 100px;
    }
    
    #cat-list li {
      cursor: pointer;
    }
    
    #cat-container {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    
    #cat-img {
      width: 300px;
      height: 300px;
    }
    
    #likes-btn {
      cursor: pointer;
    }

    Model

    let model = {
        currentCat: null,
      cats: [
          {
            title: '我是一号喵喵',
          imageSrc: 'https://placekitten.com/300/300',
          likesCount: 0
        },
        {
            title: '我是二号喵喵',
          imageSrc: 'https://placekitten.com/301/301',
          likesCount: 0
        },
          {
            title: '我是三号喵喵',
          imageSrc: 'https://placekitten.com/302/302',
          likesCount: 0
        },
          {
            title: '我是四号喵喵',
          imageSrc: 'https://placekitten.com/303/303',
          likesCount: 0
        }
      ]
    };

    我们的数据包括两部分,currentCat表示当前展示的猫咪详情对象, cats表示猫咪列表。可以看出我们现在的Model就是单纯的数据展示,没有任何与展示相关的逻辑,未来拓展起来非常方便。

    Bridge

    刚刚说了Bridge要负责为View提供所有对Model数据的读取和修改的方法。所以我们可以思考下,有哪些方法需要提供。

    1. 首先要提供一个Init的方法,执行首次渲染view的工作
    2. 获取当前选择的猫咪详情
    3. 才列表选择要浏览的猫咪
    4. 获取猫咪列表
    5. 为当前选择的猫咪点赞
    let brain = {
        init: () => {
            model.currentCat = model.cats[0]; //初始化
        
            catListView.init(); //这两个View稍后提供
            catView.init();
        },
    
        getCurrentCat: () => model.currentCat,
      
        setCurrentCat: (cat) => {
            model.currentCat = cat;
          catView.render();   // 手动触发View重新渲染,这是未来我们的主要优化点
        },
      
        getCats: () => model.cats,
      
         incrementLikes: () => {
            model.currentCat.likesCount++;
          catView.render();   // 手动触发View重新渲染,这是未来我们的主要优化点
        }
    };

    View

    我们有两个View,一个是猫咪列表catListView, 另一个是猫咪详情catView。

    let catView = {
        init: function () {
            this.catTitle = document.getElementById('cat-title');
            this.catImg = document.getElementById('cat-img');
            this.likesCount = document.getElementById('likes-count');
            this.likesBtn = document.getElementById('likes-btn');
        
            this.likesBtn.addEventListener('click', () => {
                brain.incrementLikes();
            })
        
            this.render()
      },
      
      render: function () {
          let currentCat = brain.getCurrentCat();
          this.catTitle.textContent = currentCat.title;
          this.catImg.src = currentCat.imageSrc;
          this.likesCount.textContent = currentCat.likesCount;
      }
    }

    注意到,我们把init和render拆成两个方法。

    init方法首先把相关的DOM节点先保存下来,避免后续每次还得重新寻找DOM节点;然后为点赞按钮绑定点击事件,点击时调用brain提供的incrementLikes方法修改Model;最后执行render完成首次渲染。

    而render函数只管渲染,每次调用都会直接修改DOM节点,更新渲染。

    let catListView = {
        init: function () {
            this.catListElem = document.getElementById('cat-list');
            this.render();
        },
      
        render: function () {
            let cats = brain.getCats();
        
            cats.forEach(cat => {
                let catElem = document.createElement('li');
                catElem.textContent = cat.title;
          
                catElem.addEventListener('click', () => {
                      brain.setCurrentCat(cat);
                  })
          
                this.catListElem.appendChild(catElem)
            })
        }
    }

    catListView结构与上面类似。

    设置完model, brain, catlistview, catview四个对象后,我们在最后调用brain.init()就完成了所有工作。

    最后

    今天我们就实现了一个MV模式,它基本上概括了MV模式的核心。但是遗留了一个很重要的问题:每当我们数据修改的时候,我们都手动调用了一下view的render函数来重新渲染页面,这样显然是不聪明的。下期我们仔细研究一下MVVM的代表Backbone, knockout等等以及Vue是怎么实现他们的MV*。

    你可能感兴趣的:(测试,前端,javascript,ViewUI)