前言:看本文之前需要了解最基本的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').innerHTMLdocument.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', 2)let bookNum = parseInt(document.querySelector('.bookNum').innerText, 10)document.querySelector('#addOne'). = function() {
bookNum += 1
document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', bookNum)
}document.querySelector('#reduceOne'). = function() {
bookNum -= 1
document.querySelector('.booksContent').innerHTML = content.replace('__bookNum__', bookNum)
}document.querySelector('#reset'). = 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 class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="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'). = 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'). = 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'). = 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 class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="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 ,所以……话不多说,我们直接改代码,步骤如下:
首先我们先引入Vue 。
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>将我们之前写的 View 类删掉,直接将其替换成 Vue ,即
view = new Vue()Vue 中 template的标记和我们的不一样,Vue使用的是
{{xx}},xx是会替换的元素。Vue 中的
template只能有一个根元素,如果template有两个根元素, Vue 只会看第一个。Vue 要求要将
data放在view 层,而不是 Model 层 ,因为 Vue 需要根据data来初始化template。上一节的代码,我们的 view 层需要有一个
render方法来渲染页面,使用 Vue 就不需要啦,Vue 有自动渲染的机制 。即我们直接改 view 层的data,html 会自动变更。而Vue 的
data中的属性怎么改呢?Vue 会把data里面的所有属性升级到当前的 view 层上。所以我们修改的是 view 层上的 属性,比如:number属性在我们之前的代码中,本应在this.view.data.number(×)上,但实际上我们应该这么改this.view.number = xxx(√)。需要注意的是:Vue 去替换或更新 html 中元素时,它不是一下全部更新,而是是更新局部需要变化的地方。比如之前的代码,数据一有变动,id 为
app里面的元素会全部重新渲染一遍,包括本来不需要变动的地方如按钮;而 Vue 更新数据,只会更新 id 为number的span,其他的地方就不会再次渲染了。Vue 甚至可以让你不需要 controller 层。所以我们可以把原先 controller 层中的所有相关操作都放在 view 层的
methods上。而且我们不需要bindEvents,因为 Vue 内置,然后直接在template中的按钮上绑定对应的事件即可。那么没有了 controller 层,我们就需要在 view 层中进行第一步的获取数据然后初始化页面。我们可以在 view 层的
created属性(为一个函数)中调用。我们仿佛一直在做赋值和取值这两件事情。Vue 这个框架就是让原来 MVC 中的 view 层更智能,然后 controller 层就可以合并到 view 层中 。
更高端的改法:可以用户输入数据来操作加几,所以在
template中添加一个input,将输入框的value数据n绑定,其中n在data中存储。Vue 的双向绑定:上一步中添加了
input,而我们一旦改了input中的值,Vue 就会发现input中的value的值变了,然后它就会去改 view 层中的data中的n,然后发现n变了后,页面中使用了n的地方的值即可就会改变,这就是Vue 的双向绑定。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的示意图让大家更直观的认识它:
图来源于阮一峰博客
作者:EnochQin
链接:https://www.jianshu.com/p/63fe81bd9671
共同學習,寫下你的評論
評論加載中...
作者其他優質文章

