MVVM
什么是MVVM?MVVM是Model-View-ViewModel的缩写。要编写可维护的前端代码绝非易事。前面已经用MVC模式通过koa实现了后端数据、模板页面和控制器的分离,但是,对于前端来说,还不够。
当用Node.js有了一整套后端开发模型后,我们对前端开发也会有新的认识。由于前端开发混合了HTML、CSS和JavaScript,而且页面众多,所以,代码的组织和维护难度其实更加复杂,这就是MVVM出现的原因。
在了解MVVM之前,先回顾一下前端发展的历史。在上个世纪的1989年,欧洲核子研究中心的物理学家Tim Berners-Lee发明了超文本标记语言(HyperText Markup Language),简称HTML,并在1993年成为互联网草案。从此,互联网开始迅速商业化,诞生了一大批商业网站。
最早的HTML页面是完全静态的网页,它们是预先编写好的存放在Web服务器上的html文件。浏览器请求某个URL时,Web服务器把对应的html文件扔给浏览器,就可以显示html文件的内容了。
如果要针对不同的用户显示不同的页面,显然不可能给成千上万的用户准备好成千上万的不同的html文件,所以,服务器就需要针对不同的用户,动态生成不同的html文件。一个最直接的想法就是利用C、C++这些编程语言,直接向浏览器输出拼接后的字符串。这种技术被称为公共网关接口CGI:Common Gateway Interface。
很显然,像新浪首页这样的复杂的HTML是不可能通过拼字符串得到的。于是,人们又发现,其实拼字符串的时候,大多数字符串都是HTML片段,是不变的,变化的只有少数和用户相关的数据,所以,又出现了新的创建动态HTML的方式:ASP、JSP和PHP——分别由微软、SUN和开源社区开发。
在ASP中,一个asp文件就是一个HTML,但是,需要替换的变量用特殊的<%=var%>
标记出来了,再配合循环、条件判断,创建动态HTML就比CGI要容易得多。但是,一旦浏览器显示了一个HTML页面,要更新页面内容,唯一的方法就是重新向服务器获取一份新的HTML内容。
有了JavaScript后,浏览器就可以运行JavaScript,然后,对页面进行一些修改。JavaScript还可以通过修改HTML的DOM结构和CSS来实现一些动画效果,而这些功能没法通过服务器完成,必须在浏览器实现。
用JavaScript在浏览器中操作HTML,经历了若干发展阶段:
第一阶段,直接用JavaScript操作DOM节点,使用浏览器提供的原生API:
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
第二阶段,由于原生API不好用,还要考虑浏览器兼容性,jQuery横空出世,以简洁的API迅速俘获了前端开发者的芳心:
$('#name').text('Homer').css('color', 'red');
第三阶段,MVC模式,需要服务器端配合,JavaScript可以在前端修改服务器渲染后的数据。
现在,随着前端页面越来越复杂,用户对于交互性要求也越来越高,想要写出Gmail这样的页面,仅仅用jQuery是远远不够的。MVVM模型应运而生。MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。
一个MVVM框架和jQuery操作DOM相比有什么区别?
先看用jQuery实现修改两个DOM节点的例子:
<!-- HTML -->
<p>Hello, <span id="name">Bart</span>!</p>
<p>You are <span id="age">12</span>.</p>
Hello, Bart!
You are 12.
用jQuery修改name和age节点的内容:
'use strict';
var name = 'Home';
var age = 51;
$('#name').text(name);
$('#age').text(age);
// 执行代码并观察页面变化
Hello, Home!
You are 51.
如果使用MVVM框架来实现同样的功能,首先并不关心DOM的结构,而是关心数据如何存储。最简单的数据存储方式是使用JavaScript对象:
var person = {
name: 'Bart',
age: 12
};
我们把变量person看作Model,把HTML某些DOM节点看作View,并假定它们之间被关联起来了。要把显示的name从Bart改为Home,把显示的age从12改为51,我们并不操作DOM,而是直接修改JavaScript对象:
'use strict';
person.name = 'Home';
person.age = 51;
// 执行代码并观察页面变化
Hello, Home!
You are 51.
执行上面的代码,我们发现,改变JavaScript对象的状态,会导致DOM结构作出对应的变化!这让我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了!这就是MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来!
单向绑定
MVVM就是在前端页面上,应用了扩展的MVC模式,我们关心Model的变化,MVVM框架自动把Model的变化映射到DOM结构上,这样,用户看到的页面内容就会随着Model的变化而更新。
例如,定义好一个JavaScript对象作为Model,并且把这个Model的两个属性绑定到DOM节点上:
经过MVVM框架的自动转换,浏览器就可以直接显示Model的数据了,如下所示:
目前,常用的MVVM框架有:
- Angular:Google出品,名气大,但是很难用;
- Backbone.js:入门非常困难,因为自身API太多;
- Ember:一个大而全的框架,想写个Hello world都很困难。
选择MVVM的目标应该是入门容易,安装简单,能直接在页面写JavaScript,需要更复杂的功能时又能扩展支持。所以,综合考察,最佳选择是MVVM框架:Vue.js
首先创建基于koa的Node.js项目。虽然目前我们只需要在HTML静态页面中编写MVVM,但是很快我们就需要和后端API进行交互,因此,要创建基于koa的项目结构如下:
hello-vue/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- app.js <-- koa app
|
+- static-files.js <-- 支持静态文件的koa middleware
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
|
+- static/ <-- 存放静态资源文件
|
+- css/ <-- 存放bootstrap.css等
|
+- fonts/ <-- 存放字体文件
|
+- js/ <-- 存放各种js文件
|
+- index.html <-- 使用MVVM的静态页面
这个Node.js项目的主要目的是作为服务器输出静态网页,因此,package.json仅需要如下依赖包:
"dependencies": {
"koa": "2.0.0",
"mime": "1.3.4",
"mz": "2.4.0"
}
使用npm install安装好依赖包,然后启动app.js,在index.html文件中随便写点内容,确保浏览器可以通过http://localhost:3000/static/index.html访问到该静态文件。紧接着,在index.html中用Vue实现MVVM的一个简单例子。
安装Vue
安装Vue有很多方法,可以用npm或者webpack。但是现在的目标是尽快用起来,所以最简单的方法是直接在HTML代码中像引用jQuery一样引用Vue。可以直接使用CDN的地址,例如:
<script src="https://unpkg.com/vue@2.0.1/dist/vue.js"></script>
也可以把vue.js文件下载下来,放到项目的/static/js文件夹中,使用本地路径:
<script src="/static/js/vue.js"></script>
这里需要注意,vue.js是未压缩的用于开发的版本,它会在浏览器console中输出很多有用的信息,帮助我们调试代码。当开发完毕,需要真正发布到服务器时,应该使用压缩过的vue.min.js,它会移除所有调试信息,并且文件体积更小。
编写MVVM
下一步,就可以在HTML页面中编写JavaScript代码了。我们的Model是一个JavaScript对象,它包含两个属性:
{
name: 'Robot',
age: 15
}
而负责显示的是DOM节点可以用{{ name }}
和{{ age}}
来引用Model的属性:
<div id="vm">
<p>Hello, {{ name }}!</p>
<p>You are {{ age }} years old!</p>
</div>
最后一步是用Vue把两者关联起来。要特别注意的是,在<head>
内部编写的JavaScript代码,需要用jQuery把MVVM的初始化代码推迟到页面加载完毕后执行,否则,直接在<head>
内执行MVVM代码时,DOM节点尚未被浏览器加载,初始化将失败。正确的写法如下:
<html>
<head>
<!-- 引用jQuery -->
<script src="/static/js/jquery.min.js"></script>
<!-- 引用Vue -->
<script src="/static/js/vue.js"></script>
<script>
// 初始化代码:
$(function () {
var vm = new Vue({
el: '#vm',
data: {
name: 'Robot',
age: 15
}
});
window.vm = vm;
});
</script>
</head>
<body>
<div id="vm">
<p>Hello, {{ name }}!</p>
<p>You are {{ age }} years old!</p>
</div>
</body>
<html>
我们创建一个VM的核心代码如下:
var vm = new Vue({
el: '#vm',
data: {
name: 'Robot',
age: 15
}
});
其中,el指定了要把Model绑定到哪个DOM根节点上,语法和jQuery类似。这里的’#vm’对应ID为vm的一个<div>
节点:
<div id="vm">
...
</div>
在该节点以及该节点内部,就是Vue可以操作的View。Vue可以自动把Model的状态映射到View上,但是不能操作View范围之外的其他DOM节点。
然后,data属性指定了Model,我们初始化了Model的两个属性name和age,在View内部的<p>
节点上,可以直接用{{ name }}引用Model的某个属性,如下所示:
<div id="vm">
<p>Hello, {{ name }}!</p>
<p>You are {{ age }} years old!</p>
</div>
一切正常的话,我们在浏览器中访问http://localhost:3000/static/index.html,可以看到页面输出为:
Hello, Robot!
You are 15 years old!
如果打开浏览器console,因为我们用代码window.vm = vm,把VM变量绑定到了window对象上,所以,可以直接修改VM的Model:
window.vm.name = 'Bob'
执行上述代码,可以观察到页面立刻发生了变化,原来的Hello, Robot!自动变成了Hello, Bob!。Vue作为MVVM框架会自动监听Model的任何变化,在Model数据变化时,更新View的显示。这种Model到View的绑定我们称为单向绑定。经过CSS修饰后的页面最终效果如下:
可以在页面直接输入JavaScript代码改变Model,并观察页面变化。
单向绑定
在Vue中,可以直接写{{ name }}
绑定某个属性。如果属性关联的是对象,还可以用多个.
引用,例如,{{ address.zipcode }}
。
另一种单向绑定的方法是使用Vue的指令v-text,写法如下:
<p>Hello, <span v-text="name"></span>!</p>
这种写法是把指令写在HTML节点的属性上,它会被Vue解析,该节点的文本内容会被绑定为Model的指定属性,注意不能再写双花括号{{ }}
。
双向绑定
单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。有单向绑定,就有双向绑定。如果用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。
什么情况下用户可以更新View呢? 填写表单就是一个最直接的例子。当用户填写表单时,View的状态就被更新了,如果此时MVVM框架可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定:
在浏览器中,当用户修改了表单的内容时,我们绑定的Model会自动更新:
在Vue中,使用双向绑定非常容易,仍然先创建一个VM实例:
$(function () {
var vm = new Vue({
el: '#vm',
data: {
email: '',
name: ''
}
});
window.vm = vm;
});
然后,编写一个HTML FORM表单,并用v-model指令把某个<input>
和Model的某个属性作双向绑定:
<form id="vm" action="#">
<p><input v-model="email"></p>
<p><input v-model="name"></p>
</form>
我们可以在表单中输入内容,然后在浏览器console中用window.vm.$data
查看Model的内容,也可以用window.vm.name
查看Model的name属性,它的值和FORM表单对应的<input>
是一致的。
如果在浏览器console中用JavaScript更新Model,例如,执行window.vm.name='Bob'
,表单对应的<input>
内容就会立刻更新。除了<input type="text">
可以和字符串类型的属性绑定外,其他类型的<input>
也可以和相应数据类型绑定,例如多个checkbox可以和数组绑定:
<label><input type="checkbox" v-model="language" value="zh"> Chinese</label>
<label><input type="checkbox" v-model="language" value="en"> English</label>
<label><input type="checkbox" v-model="language" value="fr"> French</label>
对应的Model为:
language: ['zh', 'en']
单个checkbox可以和boolean类型变量绑定:
<input type="checkbox" v-model="subscribe">
对应的Model为:
subscribe: true; // 根据checkbox是否选中为true/false
下拉框<select>
绑定的是字符串,但是要注意,绑定的是value而非用户看到的文本:
<select v-model="city">
<option value="bj">Beijing</option>
<option value="sh">Shanghai</option>
<option value="gz">Guangzhou</option>
</select>
对应的Model为:
city: 'bj' // 对应option的value
双向绑定最大的好处是我们不再需要用jQuery去查询表单的状态,而是直接获得了用JavaScript对象表示的Model。
处理事件
当用户提交表单时,传统的做法是响应onsubmit事件,用jQuery获取表单内容,检查输入是否有效,最后提交表单,或者用AJAX提交表单。
现在,获取表单内容已经不需要了,因为双向绑定直接让我们获得了表单内容,并且获得了合适的数据类型。响应onsubmit事件也可以放到VM中。我们在<form>
元素上使用指令:
<form id="vm" v-on:submit.prevent="register">
其中,v-on:submit="register"
指令就会自动监听表单的submit事件,并调用register方法处理该事件。使用.prevent表示阻止事件冒泡,这样,浏览器不再处理<form>
的submit事件。
因为我们指定了事件处理函数是register,所以需要在创建VM时添加一个register函数:
var vm = new Vue({
el: '#vm',
data: {
...
},
methods: {
register: function () {
// 显示JSON格式的Model:
alert(JSON.stringify(this.$data));
// TODO: AJAX POST...
}
}
});
在register()函数内部,我们可以用AJAX把JSON格式的Model发送给服务器,就完成了用户注册的功能。使用CSS修饰后的页面效果如下:
同步DOM结构
除了简单的单向绑定和双向绑定,MVVM还有一个重要的用途,就是让Model和DOM的结构保持同步。用一个TODO的列表作为示例,从用户角度看,一个TODO列表在DOM结构的表现形式就是一组<li>
节点:
<ol>
<li>
<dl>
<dt>产品评审</dt>
<dd>新款iPhone上市前评审</dd>
</dl>
</li>
<li>
<dl>
<dt>开发计划</dt>
<dd>与PM确定下一版Android开发计划</dd>
</dl>
</li>
<li>
<dl>
<dt>VC会议</dt>
<dd>敲定C轮5000万美元融资</dd>
</dl>
</li>
</ol>
而对应的Model可以用JavaScript数组表示:
todos: [
{
name: '产品评审',
description: '新款iPhone上市前评审'
},
{
name: '开发计划',
description: '与PM确定下一版Android开发计划'
},
{
name: 'VC会议',
description: '敲定C轮5000万美元融资'
}
]
使用MVVM时,当我们更新Model时,DOM结构会随着Model的变化而自动更新。当todos数组增加或删除元素时,相应的DOM节点会增加<li>
或者删除<li>
节点。在Vue中,可以使用v-for
指令来实现:
<ol>
<li v-for="t in todos">
<dl>
<dt>{{ t.name }}</dt>
<dd>{{ t.description }}</dd>
</dl>
</li>
</ol>
v-for
指令把数组和一组<li>
元素绑定了。在<li>
元素内部,用循环变量t
引用某个属性,例如,{{ t.name }}
。这样,我们只关心如何更新Model,不关心如何增删DOM节点,大大简化了整个页面的逻辑。
我们可以在浏览器console中用window.vm.todos[0].name='计划有变'
查看View的变化,或者通过window.vm.todos.push({name:'新计划',description:'blabla...'})
来增加一个数组元素,从而自动添加一个<li>
元素。
需要注意的是,Vue之所以能够监听Model状态的变化,是因为JavaScript语言本身提供了Proxy
或者Object.observe()
机制来监听对象状态的变化。但是,对于数组元素的赋值,却没有办法直接监听,因此,如果我们直接对数组元素赋值:
vm.todos[0] = {
name: 'New name',
description: 'New description'
};
会导致Vue无法更新View。正确的方法是不要对数组元素赋值,而是更新:
vm.todos[0].name = 'New name';
vm.todos[0].description = 'New description';
或者,通过splice()
方法,删除某个元素后,再添加一个元素,达到“赋值”的效果:
var index = 0;
var newElement = {...};
vm.todos.splice(index, 1, newElement);
Vue可以监听数组的splice、push、unshift
等方法调用,所以,上述代码可以正确更新View。用CSS修饰后的页面效果如下:
集成API
前面用Vue实现了一个简单的TODO应用。通过对Model的更新,DOM结构可以同步更新。现在,如果要把这个简单的TODO应用变成一个用户能使用的Web应用,需要解决几个问题:
- 用户的TODO数据应该从后台读取;
- 对TODO的增删改必须同步到服务器后端;
- 用户在View上必须能够修改TODO。
第1个和第2个问题都是和API相关的。只要实现了合适的API接口,就可以在MVVM内部更新Model的同时,通过API把数据更新反映到服务器端,这样,用户数据就保存到了服务器端,下次打开页面时就可以读取TODO列表。在vue-todo的基础上创建vue-todo-2项目,结构如下:
vue-todo-2/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- app.js <-- koa app
|
+- static-files.js <-- 支持静态文件的koa middleware
|
+- controller.js <-- 支持路由的koa middleware
|
+- rest.js <-- 支持REST的koa middleware
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
|
+- controllers/ <-- 存放Controller
| |
| +- api.js <-- REST API
|
+- static/ <-- 存放静态资源文件
|
+- css/ <-- 存放bootstrap.css等
|
+- fonts/ <-- 存放字体文件
|
+- js/ <-- 存放各种js文件
|
+- index.html <-- 使用MVVM的静态页面
在api.js文件中,用数组在服务器端模拟一个数据库,然后实现以下4个API:
GET /api/todos:返回所有TODO列表;
POST /api/todos:创建一个新的TODO,并返回创建后的对象;
PUT /api/todos/:id:更新一个TODO,并返回更新后的对象;
DELETE /api/todos/:id:删除一个TODO。
和之前的TODO数据结构相比,现在需要增加一个id属性,来唯一标识一个TODO。准备好API后,在Vue中,如何把Model的更新同步到服务器端?
有两个方法,一是直接用jQuery的AJAX调用REST API,不过这种方式比较麻烦。
第二个方法是使用vue-resource这个针对Vue的扩展,它可以给VM对象加上一个$resource
属性,通过$resource
来方便地操作API。
使用vue-resource只需要在导入vue.js后,加一行<script>
导入vue-resource.min.js
文件即可。可以直接使用CDN的地址:
<script src="https://cdn.jsdelivr.net/vue.resource/1.0.3/vue-resource.min.js"></script>
给VM增加一个init()方法,读取TODO列表:
var vm = new Vue({
el: '#vm',
data: {
title: 'TODO List',
todos: []
},
created: function () {
this.init();
},
methods: {
init: function () {
var that = this;
that.$resource('/api/todos').get().then(function (resp) {
// 调用API成功时调用json()异步返回结果:
resp.json().then(function (result) {
// 更新VM的todos:
that.todos = result.todos;
});
}, function (resp) {
// 调用API失败:
alert('error');
});
}
}
});
注意到创建VM时,created指定了当VM初始化成功后的回调函数,这样,init()方法会被自动调用。
类似的,对于添加、修改、删除的操作,也需要往VM中添加对应的函数。以添加为例:
var vm = new Vue({
...
methods: {
...
create: function (todo) {
var that = this;
that.$resource('/api/todos').save(todo).then(function (resp) {
resp.json().then(function (result) {
that.todos.push(result);
});
}, showError);
},
update: function (todo, prop, e) {
...
},
remove: function (todo) {
...
}
}
});
添加操作需要一个额外的表单,我们可以创建另一个VM对象vmAdd
来对表单作双向绑定,然后,在提交表单的事件中调用vm对象的create方法:
var vmAdd = new Vue({
el: '#vmAdd',
data: {
name: '',
description: ''
},
methods: {
submit: function () {
vm.create(this.$data);
}
}
});
vmAdd和FORM表单绑定:
<form id="vmAdd" action="#0" v-on:submit.prevent="submit">
<p><input type="text" v-model="name"></p>
<p><input type="text" v-model="description"></p>
<p><button type="submit">Add</button></p>
</form>
最后,把vm绑定到对应的DOM:
<div id="vm">
<h3>{{ title }}</h3>
<ol>
<li v-for="t in todos">
<dl>
<dt contenteditable="true" v-on:blur="update(t, 'name', $event)">{{ t.name }}</dt>
<dd contenteditable="true" v-on:blur="update(t, 'description', $event)">{{ t.description }}</dd>
<dd><a href="#0" v-on:click="remove(t)">Delete</a></dd>
</dl>
</li>
</ol>
</div>
这里我们用contenteditable="true"
让DOM节点变成可编辑的,用v-on:blur="update(t, 'name', $event)"
在编辑结束时调用update()
方法并传入参数,特殊变量$event
表示DOM事件本身。删除TODO是通过对<a>
节点绑定v-on:click
事件并调用remove()
方法实现的。
如果一切无误,我们就可以先启动服务器,然后在浏览器中访问http://localhost:3000/static/index.html
,对TODO进行增删改等操作,操作结果会保存在服务器端。通过Vue和vue-resource插件,我们用简单的几十行代码就实现了一个真正可用的TODO应用。
使用CSS修饰后的页面效果如下:
后台服务器数据也相应的被删除了,如下:
在线电子表格
利用MVVM,很多非常复杂的前端页面编写起来就非常容易了。这得益于我们把注意力放在Model的结构上,而不怎么关心DOM的操作。现在学习如何利用Vue快速创建一个在线电子表格:
首先,定义Model的结构,它的主要数据就是一个二维数组,每个单元格用一个JavaScript对象表示:
data: {
title: 'New Sheet',
header: [ //对应表格首行 A, B, C...
{ row: 0, col: 0, text: '' },
{ row: 0, col: 1, text: 'A' },
{ row: 0, col: 2, text: 'B' },
{ row: 0, col: 3, text: 'C' },
...
{ row: 0, col: 10, text: 'J' }
],
rows: [
[
{ row: 1, col: 0, text: '1' },
{ row: 1, col: 1, text: '' },
{ row: 1, col: 2, text: '' },
...
{ row: 1, col: 10, text: '' },
],
[
{ row: 2, col: 0, text: '2' },
{ row: 2, col: 1, text: '' },
{ row: 2, col: 2, text: '' },
...
{ row: 2, col: 10, text: '' },
],
...
[
{ row: 10, col: 0, text: '10' },
{ row: 10, col: 1, text: '' },
{ row: 10, col: 2, text: '' },
...
{ row: 10, col: 10, text: '' },
]
],
selectedRowIndex: 0, // 当前活动单元格的row
selectedColIndex: 0 // 当前活动单元格的col
}
紧接着,我们就可以把Model的结构映射到一个<table>
上:
<table id="sheet">
<thead>
<tr>
<th v-for="cell in header" v-text="cell.text"></th>
</tr>
</thead>
<tbody>
<tr v-for="tr in rows">
<td v-for="cell in tr" v-text="cell.text"></td>
</tr>
</tbody>
</table>
现在,用Vue把Model和View关联起来,这个电子表格的原型已经可以运行了!
下一步,想在单元格内输入一些文本,怎么办?
因为不是所有单元格都可以被编辑,首行和首列不行。首行对应的是<th>
,默认是不可编辑的,首列对应的是第一列的<td>
,所以,需要判断某个<td>
是否可编辑,我们用v-bind
指令给某个DOM元素绑定对应的HTML属性:
<td v-for="cell in tr" v-bind:contenteditable="cell.contentEditable" v-text="cell.text"></td>
在Model中给每个单元格对象加上contentEditable
属性,就可以决定哪些单元格可编辑。最后,给<td>
绑定click事件,记录当前活动单元格的row和col,再绑定blur事件,在单元格内容编辑结束后更新Model:
<td v-for="cell in tr" v-on:click="focus(cell)" v-on:blur="change" ...></td>
对应的两个方法要添加到VM中:
var vm = new Vue({
...
methods: {
focus: function (cell) {
this.selectedRowIndex = cell.row;
this.selectedColIndex = cell.col;
},
change: function (e) {
// change事件传入的e是DOM事件
var
rowIndex = this.selectedRowIndex,
colIndex = this.selectedColIndex,
text;
if (rowIndex > 0 && colIndex > 0) {
text = e.target.innerText; // 获取td的innerText
this.rows[rowIndex - 1][colIndex].text = text;
}
}
}
});
现在,单元格已经可以编辑,并且用户的输入会自动更新到Model中。如果要给单元格的文本添加格式,例如,左对齐或右对齐,可以给Model对应的对象添加一个align
属性,然后用v-bind:style
绑定到<td>
上:
<td v-for="cell in tr" ... v-bind:style="{ textAlign: cell.align }"></td>
然后,创建工具栏,给左对齐、居中对齐和右对齐按钮编写click
事件代码,调用setAlign()
函数:
function setAlign(align) {
var
rowIndex = vm.selectedRowIndex,
colIndex = vm.selectedColIndex,
row, cell;
if (rowIndex > 0 && colIndex > 0) {
row = vm.rows[rowIndex - 1];
cell = row[colIndex];
cell.align = align;
}
}
// 给按钮绑定事件:
$('#cmd-left').click(function () { setAlign('left'); });
$('#cmd-center').click(function () { setAlign('center'); });
$('#cmd-right').click(function () { setAlign('right'); });
现在,点击某个单元格,再点击右对齐按钮,单元格文本就变成右对齐了。类似的,可以继续添加其他样式,例如字体、字号等。
MVVM的适用范围
从几个例子我们可以看到,MVVM最大的优势是编写前端逻辑非常复杂的页面,尤其是需要大量DOM操作的逻辑,利用MVVM可以极大地简化前端页面的逻辑。
但是MVVM不是万能的,它的目的是为了解决复杂的前端逻辑。对于以展示逻辑为主的页面,例如,新闻,博客、文档等,不能使用MVVM展示数据,因为这些页面需要被搜索引擎索引,而搜索引擎无法获取使用MVVM并通过API加载的数据。
所以,需要SEO(Search Engine Optimization)的页面,不能使用MVVM展示数据。不需要SEO的页面,如果前端逻辑复杂,就适合使用MVVM展示数据,例如,工具类页面,复杂的表单页面,用户登录后才能操作的页面等等。