前言:看本文之前需要了解最基本的MVC思想(附一篇本人之前写的MVC设计模式在JavaScript中的运用 仅供参考)。在本篇文章中,我将先用原生JavaScript做一个小例子,然后将其先使用MVC设计模式进行代码重构,然后使用Vue框架再改写一遍,最终的代码就是使用的MVVM设计模式,从而让我们更容易的理解Vue的思路,从而在学习和工作中更好的使用Vue。
注:本文后面的MVC和Vue重构后的最终代码都会贴上JSBin的链接。在本地测试可能有bug,可使用JSBin在线测试。
1、先了解本文demo的基本需求,然后打个样
我们要做一个书籍列表,包括书本的名字和数量,
有三个按钮,可以增加或减少或清零书本的数量。
大致效果就像:
从需求就能看出其实原理非常简单,所以我们先写一个最初的静态无数据库的版本:
HTML结构如下:
1
2
3
4
5
6
7
8
9
10
11
|
<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代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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__
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!-- 引入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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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
})
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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 填充,所以直接改写成:
1
2
3
4
|
<!-- 需要获取的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 类,使得我们可以多次使用 :
1
2
3
4
5
6
7
8
9
10
11
|
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 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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 类:
1
2
3
4
5
6
7
8
|
let model = new Model({
data: {
name: '',
number: 0,
id: ''
},
resource: 'book'
})
|
- Controller 层封装比较麻烦,本文就不将其封装成类了,直接声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
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))
}
}
|
1
2
3
4
|
// 调用虚拟后台函数
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 src="https://cdn.jsdelivr.net/npm/vue@2.5.17/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 层可以接删掉):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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设计模式本文就不深入介绍了,想要深入了解的可以点击下面的几个链接:
- 维基百科
- 阮一峰 - MVC,MVP 和 MVVM 的图示
(END)