前面我们介绍了用 C 语言实现 Model 的方法,由于 C 语言是静态语言,没有办法直接通过名字去访问对象的成员变量和成员函数,所以我们需要根据Model自动生成ViewModel。如果用 JS 去开发 Model 则更加简洁。
比如,demo1 的 Model,如果 JS 来实现,那是非常简洁的。只需要定义一个 ViewModel 对象,将其与 temperature 绑定,并在 data 对象中初始化 value 的值即可:
ViewModel('temperature', {
data: {
value: 20
}
});
即使一个完整的 temperature 的 Model 也不过 10 来行代码。
ViewModel('temperature_ex', {
data: {
value: 20,
applydValue: 20
},
methods: {
canApply: function(args) {
return this.applydValue != this.value;
},
apply: function(args) {
this.applydValue = this.value;
this.notifyPropsChanged();
}
},
onWillMount: function(req) {
this.value = req.value || 20;
}
});
由此可以看出,当使用 AWTK-MVVM 开发应用程序时,JS 是最好的开发语言。
为了让 View 顺利的与 Model 绑定到一起,用 JS 实现 Model 时需要遵循一些规则。
在 XML 的界面描述文件中,仍然通过 v-model 属性来指定 Model,v-model 属性的值是 JS 的文件名(无需加扩展名)。
比如,下面的例子中,Model 的实现是放在 temperature.js 文件中的:
<window v-model="temperature">
<label x="center" y="middle" w="50%" h="40" v-data:text="{value}"/>
<slider x="center" y="middle:40" w="80%" h="20" v-data:value="{value}"/>
</window>
下面的例子中,Model 的实现是放在 temperature_ex.js 文件中的:
<window v-model="temperature_ex">
<label x="0" y="middle" w="40%" h="40" v-data:text="{value}"/>
<label x="right" y="middle" w="40%" h="40" v-data:text="{error.of.value}"/>
<slider x="center" y="middle:40" w="80%" h="20" v-data:value="{value, validator=water_temp}"/>
</window>
注意,程序启动时会默认加载 app.js 文件,运行时先在内存中查找指定名称的 ViewModel,如果找不到,则加载 v-model 属性指定的 JS 文件并再次查找。
app.js 是项目所有JS文件的集合。
ViewModel 的名称通过全局函数 ViewModel() 的第一个参数指定,要求与 XML 的界面描述文件中的 v-model 属性相同,且通常与文件名保持一致。
比如在 temperature.js 中实现 ViewModel,那么 ViewModel() 函数的第一个参数就是 "temperature"。如:
ViewModel('temperature', {
});
MVVM 中通过一个全局函数 ViewModel() 来实现 ViewModel 的定义,该函数的第一个参数表示 ViewModel 的名称,第二个参数则是 ViewModel 的具体定义,接下来将详细介绍如何实现。
命名建议使用 JS 最流行的驼峰命名方式,首单词的首字母小写,其它单词的首字母大写。 不做强制性要求,但遵循同样的规则可以带来更好的一致性。
数据分为"data"类型和"couputed"类型。
- "data"类型的数据对象
通常直接使用或者赋值的数据可以定义在 data 对象中。
比如 ViewModel 有一个名为 value 的数据,其初始化时为 20,代码如下:
ViewModel('temperature', {
data: {
value: 20
}
});
如果 ViewModel 还有一个名为 applydValue 的数据,其初始化值也为 20,代码如下:
ViewModel('temperature_ex', {
data: {
value: 20,
applydValue: 20
}
});
- "computed"类型的数据对象
通常需要额外的复杂逻辑的数据可以定义在 computed 对象中,可以在数据的 get / set 函数中定义对应的逻辑。
比如 ViewModel 有一个名为 province 的数据,该数据的值改变时需同时更新 city 数据的值,代码如下:
ViewModel('address', {
...
computed : {
...
province : {
get : function() {
return this._province;
},
set : function(val) {
this._province = val;
this._city = this.city_list.split(';')[0];
}
},
...
}
});
如果数据只需一个 get 函数,可以进行简写。
比如, ViewModel 有一个名为 address 的只读数据,其值为多个数据的组合,代码如下:
ViewModel('address', {
...
computed : {
...
address : {
get : function() {
return this.province + ' ' + this.city + ' ' + this.country + ' ' + this.detail;
}
},
...
}
});
也可以定义成
ViewModel('address', {
...
computed : {
...
address : function() {
return this.province + ' ' + this.city + ' ' + this.country + ' ' + this.detail;
},
...
}
});
ViewModel 的命令定义在 methods 对象中。比如:
下面的例子中,将按钮绑定到 apply 命令上,那么在 methods 对象中就需要实现 apply 这个函数。
<window v-model="temperature_ex">
<label x="center" y="middle:-40" w="80%" h="40" v-data:text="{value}"/>
<slider x="center" y="middle" w="90%" h="20" v-data:value="{value}"/>
<button text="Apply" x="center" y="middle:40" w="40%" h="40" v-on:click="{apply}"/>
</window>
ViewModel('temperature_ex', {
data: {
value: 20,
applydValue: 20
},
methods: {
apply: function(args) {
this.applydValue = this.value;
this.notifyPropsChanged();
}
}
});
有的命令在特定的条件下才能执行,如何检查一个命令是否满足执行的条件呢?这时就需要在 methods 对象中实现一个对应的函数,该函数的名称为:命令名的首字母大写,并加上 can 前缀。
比如, apply 命令对应的可用性检查函数为 canApply。
ViewModel('temperature_ex', {
data: {
value: 20,
applydValue: 20
},
methods: {
canApply: function(args) {
return this.applydValue != this.value;
},
apply: function(args) {
this.applydValue = this.value;
this.notifyPropsChanged();
}
}
可用性检查函数是可选的,如果该函数不存在,则认为该命令始终是可以执行的。
当 ViewModel 有变化时,需要通知 View 进行更新。有以下几种方式通知 View:
- 通过返回值通知
- 命令返回 RET_OBJECT_CHANGED 表示 ViewModel 有数据发生变化,需要 View 更新。
如:
ViewModel('temperature_ex', {
methods: {
apply: function(args) {
this.applydValue = this.value;
return RET_OBJECT_CHANGED;
}
}
});
- 调用接口通知
-
调用 notifyPropsChanged() 通知 View 有数据发生变化。
-
调用 notifyItemsChanged() 通知 View 增加或删除了指定数据集合中的项目,该接口有一个 target 的可选参数,可以设置发生变化的数据集合,为空时默认为 Model 本身。
ViewModel('temperature_ex', {
methods: {
apply: function(args) {
this.applydValue = this.value;
this.notifyPropsChanged();
}
}
});
如果 ViewModel 的变化不是由命令触发的,而是由后台的定时器或者线程触发的,那就只能通过调用接口进行通知。
普通 Model 是一个 JS Object,可以在 ViewModel 的文件中定义,也可以按照 CommonJS 或者 ES6 规范在其他文件中定义并加载到 ViewModel 的文件中使用。
MVVM 默认支持通过 require + exports 的形式加载模块,比如
在 ViewModel 文件所在的目录中有一个 objects_manager.js 文件,文件中有一个名称为 globalObjectsManager 的 Model:
function ObjectsManager() {
this.items= [];
}
exports.globalObjectsManager = new ObjectsManager();
在ViewModel中可以这样使用:
var globalObjectsManager = require('objects_manager').globalObjectsManager;
ViewModel('books', {
computed: {
items: function () {
return globalObjectsManager.items;
}
}
});
如果要使用 ES6 的 import 方式加载模块,则需自行实现 build 脚本。
当 Model 有变化时,需要通知 View 进行更新。有以下几种方式通知 View:
- 获取 ViewModel 实例后调用 ViewModel 的通知接口
- 调用全局函数 getViewModels() 获取指定 ViewModel 实例(数组)。
ViewModel 的通知接口请参阅 13.2.3.2 章节。
- 调用全局的通知接口
-
调用全局函数 notifyViewPropsChanged() 触发指定 View 实例的数据变化事件。
-
调用全局函数 notifyViewItemsChanged() 触发指定 View 实例的数据集合的项目数量变化事件。
比如,对于上述 globalObjectsManager 的 insert、remove、sale 函数可以实现如下:
function ObjectsManager() {
this.items= [];
}
ObjectsManager.prototype.insert = function(index) {
var item = {
name: "book" + Math.round(Math.random() * 1000),
stock: Math.round(Math.random() * 100)
}
this.items.splice(index, 0, item);
// 也可以用如下形式:
// var vm = getViewModels()[0];
// var vm = getViewModels('')[0];
var vm = getViewModels('[0]')[0];
if (vm !== undefined) {
vm.notifyItemsChanged(this.items);
}
};
ObjectsManager.prototype.remove = function(index, count) {
this.items.splice(index, count);
// 也可以用如下形式:
// notifyViewItemsChanged(this.items);
// notifyViewItemsChanged(this.items, '');
notifyViewItemsChanged(this.items, '[0]');
};
ObjectsManager.prototype.sale = function(index) {
this.items[index].stock = this.items[index].stock - 1;
// 也可以用如下形式:
// notifyViewPropsChanged();
// notifyViewPropsChanged('');
notifyViewPropsChanged('[0]');
};
exports.globalObjectsManager = new ObjectsManager();
在ViewModel中可以这样使用:
var globalObjectsManager = require('objects_manager').globalObjectsManager;
ViewModel('books', {
computed: {
items: function () {
return globalObjectsManager.items;
}
},
methods: {
add: function () {
globalObjectsManager.insert(0);
},
canRemove: function(args) {
return args.index < this.items.length;
},
remove: function(args) {
globalObjectsManager.remove(args.index, 1);
},
canSale: function(args) {
return this.items[args.index].stock > 0;
},
sale: function(args) {
globalObjectsManager.sale(args.index);
},
canClear: function() {
return this.items.length > 0;
},
clear: function() {
globalObjectsManager.remove(0, this.items.length);
}
}
});
用 JS 实现数据格式转换器是很方便的事情,调用全局函数 ValueConverters() 与对应的转换器名称绑定即可,不需要像 C 语言一样注册到工厂。
ValueConverter 需要实现两个函数:
-
toModel 函数负责将数据转换成适合模型存储的格式。
-
toView 函数负责将数据转换成适合视图显示的格式。
比如,温度转换器名称为 "fahrenheit",实现代码如下:
ValueConverter('fahrenheit', {
toView: function (v) {
return v * 1.8 + 32;
},
toModel: function (v) {
return (v - 32) / 1.8;
}
});
默认数据格式转换器放在 value_converter.js 文件中。
用 JS 实现数据有效性验证器是很方便的事情,调用全局函数 ValueValidator() 与对应的有效性验证器名称绑定即可,不需要像 C 语言一样注册到工厂。
数据有效性验证器需要实现两个函数:
-
isValid 用于判断数据是否有效。可以提供进一步的提示信息。
-
fix 用于对无效数据进行修正(本函数是可选的)。
比如,水温有效性验证器名称为 "water_temp",实现代码如下:
ValueValidator('water_temp', {
isValid: function (v) {
if (v <= 20) {
return { result: false, message: "too low" };
} else if (v >= 60) {
return { result: false, message: "too high" };
} else {
return { result: true, message: "normal" };
}
}
});
返回结果是个 JS 对象,其成员含义如下:
- result true 表示数据有效,false 表示数据无效。
- message 表示进一步的提示信息。
默认数据格式转换器放在 value_validator.js 文件中。
如果需要在程序启动或者退出时执行一些操作,可以使用 Application() 方法注册一个程序实例,一个程序仅能调用一次 Application()。
Application() 方法接受一个 object 参数,用于指定程序的生命周期函数。目前生命周期函数有:
函数 | 说明 |
---|---|
onLaunch | 当程序初始化完成时调用,全局只触发一次。 |
onExit | 当程序退出时调用,全局只触发一次。 |
示例如下:
var globalObjectsManager = require('objects_manager').globalObjectsManager;
/**
* @class Application
* 注册一个Application实例,用于指定程序的生命周期函数,Application()方法全局只能调用一次。
*/
Application({
/**
* @method onLaunch
* 当程序初始化完成时调用,全局只触发一次。
*/
onLaunch : function() {
console.log('============== launch ===============');
if (typeof globalObjectsManager.onCreate === 'function') {
globalObjectsManager.onCreate();
}
},
/**
* @method onExit
* 当程序退出时调用,全局只触发一次。
*/
onExit : function() {
if (typeof globalObjectsManager.onDestroy === 'function') {
globalObjectsManager.onDestroy();
}
console.log('============== exit ===============');
}
});