从MVC到MVVM(初识Vue)

前言:看本文之前需要了解最基本的MVC思想(附一篇本人之前写的MVC设计模式在JavaScript中的运用 仅供参考)。在本篇文章中,我将先用原生JavaScript做一个小例子,然后将其先使用MVC设计模式进行代码重构,然后使用Vue框架再改写一遍,最终的代码就是使用的MVVM设计模式,从而让我们更容易的理解Vue的思路,从而在学习和工作中更好的使用Vue。

注:本文后面的MVC和Vue重构后的最终代码都会贴上JSBin的链接。在本地测试可能有bug,可使用JSBin在线测试。

1、先了解本文demo的基本需求,然后打个样

我们要做一个书籍列表,包括书本的名字和数量,
有三个按钮,可以增加或减少或清零书本的数量。
大致效果就像:

从需求就能看出其实原理非常简单,所以我们先写一个最初的静态无数据库的版本:
HTML结构如下:

<section class="booksList">
  <p class="booksContent">
    书名:<span class="bookName">《JavaScript高级程序设计》</span> 数量:
    <span class="bookNum">__bookNum__</span>
  </p>
  <div class="buttons">
    <button id="addOne">ADD 1</button>
    <button id="reduceOne">REDUCE 1</button>
    <button id="reset">RESET</button>
  </div>
</section>

JavaScript代码:

let content = document.querySelector('.booksContent').innerHTML
document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', 2)
let bookNum = parseInt(document.querySelector('.bookNum').innerText, 10)
document.querySelector('#addOne').onclick = function() {
  bookNum += 1
  document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', bookNum)
}
document.querySelector('#reduceOne').onclick = function() {
  bookNum -= 1
  document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', bookNum)
}
document.querySelector('#reset').onclick = function() {
  bookNum = 0
  document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', bookNum)
}

上面的代码能实现最起初的需求,但是这还不够!这种写法我们其实修改的是HTML中的数据,无论怎么点击按钮,网页一刷新,书的数量还是默认的2,这就很无趣。

在实际开发中,我们的书名以及书的数量应该从后台的数据库中读取,所以我们应该有ajax请求。所以请看下一步:

2、认识一个新朋友:axios

既然需要后台,就得有AJAX,那么就得有jQuery,但是今天我不准备使用jQuery,所以就要引入本节的标题:axios ,这是一个专门用来实现AJAX的库,它基于Promise的HTTP客户端,用于浏览器和node.js 。基本使用方法和jQuery类似,当然提供了更多的功能,比如axios.post()axios.get()axios.put()axios.patch()axios.delete()等,这个库除了AJAX功能外就没有其他的功能了,所以也可以说它更专注。

我们为什么要用这个库呢?主要还是为了后面的转到Vue时更容易理解,使用Vue这个框架时,一般都是使用axios来操作AJAX,Vue的作者也推荐使用。至此我们就可以完全抛弃jQuery了。

关于axios库本文不做深入介绍,可进入axios的github主页查看相关文档。

3、使用axios 假装做一个后台

为什么要说假装呢?因为我们确实没有服务器来做后台,而axios提供了一个可以劫持当前网页请求的api,即axios.interceptors

  • 相当于劫持了网页中发送的请求request,以及响应response,所以请求没有真的被后台的逻辑处理,而是axios通过interceptors这个api使用该方法下的逻辑自己处理这个请求后返回给页面一个假的响应。

所以,我们的代码就可以是这样的:

  • HTML中只需引入axios库,然后让书名也从数据库中取得,所以将书名先写成__bookName__方便替换,书的数量还是__bookNum__
<!-- 引入axios库 -->
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<!-- HTML结构 -->
<section class="booksList">
  <p class="booksContent">
    书名:<span class="bookName">__bookName__</span> 数量:
    <span class="bookNum">__bookNum__</span>
  </p>
  <div class="buttons">
    <button id="addOne">ADD 1</button>
    <button id="reduceOne">REDUCE 1</button>
    <button id="reset">RESET</button>
  </div>
</section>
  • 假装有后台的代码,将其封装成了一个函数fakeData
function fakeData() {
  // 一个假的数据库book
  let book = {
    name: 'JavaScript 高级程序设计',
    number: 2,
    id: 1
  }
  // 在真正返回response之前使用
  axios.interceptors.response.use(function(response) {
    // 获取请求的数据
    let {config: {method, url, data}} = response
    if (url === '/books/1' && method === 'get') {
      response.data = book
    } else if (url === '/books/1' && method === 'put') {
      data = JSON.parse(data)
      // 如果是PUT请求,说明要改后台数据,因此将数据库book中的数据部分更新即可
     
      Object.assign(book, data)
      response.data = book
      console.log(book) //将数据库打印出来 
    }
    return response
  })
}
  • JavaScript使用后台请求的版本:
fakeData()
axios.get('/books/1').then(({data})=>{
  let originalContent = document.querySelector('.booksContent').innerHTML
  let newContent = originalContent.replace('__bookName__',data.name).replace('__bookNum__',data.number)
  document.querySelector('.booksContent').innerHTML = newContent
})

document.querySelector('#addOne').onclick = function() {
  let bookNum = parseInt(document.querySelector('.bookNum').innerText, 10)
  bookNum += 1
  axios.put('/books/1',{num:bookNum}).then(()=>{
    document.querySelector('.bookNum').innerHTML = bookNum
  })
}
document.querySelector('#reduceOne').onclick = function() {
  let bookNum = parseInt(document.querySelector('.bookNum').innerText, 10)
  bookNum -= 1
   axios.put('/books/1',{num:bookNum}).then(()=>{
    document.querySelector('.bookNum').innerHTML = bookNum
  })
}
document.querySelector('#reset').onclick = function() {
  let bookNum = parseInt(document.querySelector('.bookNum').innerText, 10)
  bookNum = 0
   axios.put('/books/1',{num:bookNum}).then(()=>{
    document.querySelector('.bookNum').innerHTML = bookNum
  })
}

然后我们会发现,即使是这么小的需求,居然都写成了看起来如此混乱的代码,有大量重复的逻辑,业界给这种代码取名为意大利面条式代码,这种代码非常难以维护,简直人神共愤,因此为了小伙伴们的人身安全,我们还是使用MVC的思想来改写一下吧。

4、使用MVC设计模式改写代码

我们知道MVC设计模式分为三部分:

  • Model层负责数据管理,包括数据逻辑、数据请求、数据存储等功能。前端 Model 主要负责 AJAX 请求或者 LocalStorage 存储
  • View是表现层,负责用户界面,前端 View 主要负责 HTML 渲染。
  • Controller 层负责处理View 的事件,并更新 Model;也负责监听 Model的变化,并更新 View,Controller 控制其他的所有流程。

因此,我们将上面的JavaScript代码进行如下改写:

  • HTML 中我们可以让页面中的元素由 JS 填充,所以直接改写成:
<!-- 需要获取的DOM元素太多,元素JS比较麻烦,直接引入jQuery -->
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<!--body中只需要这一行即可,内容由JavaScript来填充-->
<section id="app"></section>
  • Vew 层代码:
    封装一个 View 类,使得我们可以多次使用 :
function View({el, template}){
  this.el = el
  this.template = template
}
View.prototype.render = function(data){
  let html = this.template
  for(let key in data){
    html = html.replace(`__${key}__`, data[key])
  }
  $(this.el).html(html)
}

使用 View 类:

let view = new View({
  el: '#app',
  template: `
    <div>
    书名:《__name__》
    数量:<span id=number>__number__</span>
    </div>
    <div>
      <button id="addOne">加1</button>
      <button id="reduceOne">减1</button>
      <button id="reset">归零</button>
    </div>
  `
})
  • Model 层代码:
    先封装一个 Model 类:
function Model(options){
  this.data = options.data
  this.resource = options.resource
}
Model.prototype.fetch = function(id){
  return axios.get(`/${this.resource}s/${id}`).then((response) => {
      this.data = response.data
      return response
    })
}
Model.prototype.update = function(data){
  let id = this.data.id
    return axios.put(`/${this.resource}s/${id}`, data).then((response) => {
      this.data = response.data 
      return response
    })
}

使用这个 Model 类:

let model = new Model({
  data: {
    name: '',
    number: 0,
    id: ''
  },
  resource: 'book'
})
  • Controller 层封装比较麻烦,本文就不将其封装成类了,直接声明:
let controller = {
  init({view,model}){
    this.view = view
    this.model = model
    this.view.render(this.model.data)
    this.bindEvents()
    this.model.fetch(1).then(() => {
      this.view.render(this.model.data)
    })
  },
  addOne() {
    var oldNumber = $('#number').text() // string
    var newNumber = oldNumber - 0 + 1
    this.model.update({
      number: newNumber
    }).then(() => {
      this.view.render(this.model.data)
    })

  },
  reduceOne() {
    var oldNumber = $('#number').text() // string
    var newNumber = oldNumber - 0 - 1
    this.model.update({
      number: newNumber
    }).then(() => {
      this.view.render(this.model.data)
    })
  },
  reset() {
    this.model.update({
      number: 0
    }).then(() => {
      this.view.render(this.model.data)
    })
  },
  bindEvents() {
    // this === controller
    $(this.view.el).on('click', '#addOne', this.addOne.bind(this))
    $(this.view.el).on('click', '#reduceOne', this.reduceOne.bind(this))
    $(this.view.el).on('click', '#reset', this.reset.bind(this))
  }
}
  • 最后直接调用各个函数即可:
// 调用虚拟后台函数
fakeData()
// 调用 controller 中的 init 函数
controller.init({view:view, model: model})

本节完整代码:https://jsbin.com/zetuni/edit?html,js,output

5、直接将MVC模式的代码用 Vue 改写一下吧

首先我们要了解一点,Vue 框架,所代替的就是上文中 MVC 设计模式中的 View ,所以……话不多说,我们直接改代码,步骤如下:

  1. 首先我们先引入Vue 。
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
  2. 将我们之前写的 View 类删掉,直接将其替换成 Vue ,即 view = new Vue()
  3. Vue 中 template的标记和我们的不一样,Vue使用的是{{xx}},xx是会替换的元素。
  4. Vue 中的 template 只能有一个根元素,如果 template 有两个根元素, Vue 只会看第一个。
  5. Vue 要求要将 data 放在view 层,而不是 Model 层 ,因为 Vue 需要根据 data 来初始化 template
  6. 上一节的代码,我们的 view 层需要有一个 render 方法来渲染页面,使用 Vue 就不需要啦,Vue 有自动渲染的机制 。即我们直接改 view 层的 data ,html 会自动变更。
  7. 而Vue 的 data 中的属性怎么改呢?Vue 会把 data 里面的所有属性升级到当前的 view 层上。所以我们修改的是 view 层上的 属性,比如:number 属性在我们之前的代码中,本应在 this.view.data.number (×)上,但实际上我们应该这么改 this.view.number = xxx (√)。
  8. 需要注意的是:Vue 去替换或更新 html 中元素时,它不是一下全部更新,而是是更新局部需要变化的地方。比如之前的代码,数据一有变动,id 为app 里面的元素会全部重新渲染一遍,包括本来不需要变动的地方如按钮;而 Vue 更新数据,只会更新 id 为 numberspan,其他的地方就不会再次渲染了。
  9. Vue 甚至可以让你不需要 controller 层。所以我们可以把原先 controller 层中的所有相关操作都放在 view 层的 methods 上。而且我们不需要 bindEvents ,因为 Vue 内置,然后直接在 template 中的按钮上绑定对应的事件即可。
  10. 那么没有了 controller 层,我们就需要在 view 层中进行第一步的获取数据然后初始化页面。我们可以在 view 层的 created 属性(为一个函数)中调用。
  11. 我们仿佛一直在做赋值和取值这两件事情。Vue 这个框架就是让原来 MVC 中的 view 层更智能,然后 controller 层就可以合并到 view 层中 。
  12. 更高端的改法:可以用户输入数据来操作加几,所以在 template 中添加一个input,将输入框的value数据n绑定,其中ndata 中存储。
  13. Vue 的双向绑定:上一步中添加了input,而我们一旦改了 input 中的值,Vue 就会发现 input 中的 value 的值变了,然后它就会去改 view 层中的 data 中的 n ,然后发现 n 变了后,页面中使用了 n 的地方的值即可就会改变,这就是Vue 的双向绑定。
  14. Vue 也就是自动化的 MVC,所以也被称为 MVVM 。

改完后的完整代码(https://jsbin.com/dapotuj/edit?js,output
其中把JS部分中的view 层代码贴到下面(model 层不变,controller 层可以接删掉):

let view = new Vue({
  el: '#app',
  data: {
    book: {
      name:'未命名',
      number: 0,
      id: ''
    },
    n:1
  },
  template: `
    <div>
      <div>
      书名:《{{book.name}}》
      数量:<span id=number>{{book.number}}</span>
      </div>
      <div>
        <input v-model="n" />
        <span>N的值为:{{n}}</span>
      </div>
      <div>
        <button v-on:click="addOne">加N</button>
        <button v-on:click="reduceOne">减N</button>
        <button v-on:click="reset">归零</button>
      </div>
    </div>
  `,
  created(){
    model.fetch(1).then(()=>{
      this.book = model.data
    })
  },
  methods: {
    addOne() {
      model.update({
        number: this.book.number + parseInt(this.n,10)
      }).then(() => {
        this.view.book = this.model.data
      })
    },
    reduceOne() {
      model.update({
        number: this.book.number - parseInt(this.n,10)
      }).then(() => {
        this.view.book = this.model.data
      })
    },
    reset() {
      model.update({
        number: 0
      }).then(() => {
        this.view.book = this.model.data
      })
    }
  }
})

6、出现了,传说中的 MVVM

上一节使用 Vue 改写后的代码,其实就是将传统的MVC变成使用 Vue 的MVC,也就是传说中的 MVVM,下面就来简单的介绍一下 MVVM 设计模式。

MVVM 模式有四个组成部分,分别是:

  • 模型
    是指代表真实状态内容的领域模型(面向对象),或指代表内容的数据访问层(以数据为中心)。

  • 视图
    就像在MVC模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。

  • 视图模型
    视图模型是暴露公共属性和命令的视图的抽象。MVVM没有MVC模式的控制器,也没有MVP模式的presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通信。

  • 绑定器
    声明性数据和命令绑定隐含在MVVM模式中。在Microsoft解决方案堆中,绑定器是一种名为XAML。绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。

选取阮一峰关于MVVM的示意图让大家更直观的认识它:

图来源于阮一峰博客

关于MVVM设计模式本文就不深入介绍了,想要深入了解的可以点击下面的几个链接:

  1. 维基百科
  2. 阮一峰 - MVC,MVP 和 MVVM 的图示

(END)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,252评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,886评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,814评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,869评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,888评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,475评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,010评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,924评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,469评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,552评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,680评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,362评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,037评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,519评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,621评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,099评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,691评论 2 361

推荐阅读更多精彩内容