# 5. MVVM-JS案例分析
上一章介绍了如何制作 MVVM-JS 项目,接下来本章将以 Mvvm-JS-Demo 为例,详细介绍使用 AWTK Designer 开发 MVVM-JS 项目过程,加深大家对 MVVM-JS 的理解,该案例的界面和功能跟第三章中介绍的 Mvvm-C-Demo 一样,此处不过多介绍,具体详见上文第三章,下文中将详细介绍二者在实现语言上的区别。
# 5.1 模型设计
首先使用 AWTK Designer 新建一个 MVVM-JS 项目工程,步骤详见上文 4.1 章节。根据 Mvvm-JS-Demo 的功能需求,此处我们需要添加一个名为"device"的设备模型,该模型与 3.1 章节中的介绍类似,包含设备类型、设备参数和数据,但由于底层采用 JS 语言实现,因此数据类型与 Mvvm-C-Demo 中的略微不同,详见下表:
属性名称 | 类型 | 描述 | 备注 |
---|---|---|---|
pack_type | Number | 设备类型 | 由于MVVM-JS项目暂不支持自定义枚举类型,此处使用 Number 类型代替 |
pack_params | Number | 设备参数 | 由于MVVM-JS项目暂不支持自定义枚举类型,此处使用 Number 类型代替 |
io1 | Boolean | 设备数据 | 随机生成,由设备类型决定是否显示 |
io1 | Boolean | 设备数据 | 随机生成,由设备类型决定是否显示 |
a1 | Number | 设备数据 | 随机生成,由设备类型决定是否显示 |
a2 | Number | 设备数据 | 随机生成,由设备类型决定是否显示 |
temp | Number | 设备数据 | 随机生成,由设备类型决定是否显示 |
tps | Number | 设备数据 | 随机生成,由设备类型决定是否显示 |
由于 JS 中暂不支持自定义枚举类型,本案例中"device"模型的 pack_type 属性和 pack_params 属性都采用 JS 中的 Number 类型代替,其中 pack_type 属性将决定该设备显示哪些数据,为了方便演示效果,这些设备数据均是随机生成的。
# 5.1.1 新增设备模型
根据案例需求,此处需要新增一个设备模型,名称为"device",步骤详见上文 4.3.1 章节,效果如下图所示:

在 AWTK Designer 新增 device 模型保存后,会自动在项目目录的"src/models"文件夹中生成 device 模型的相关代码,此处采用 JS 语言实现,模型代码非常简洁,其中 constructor() 函数为 device 的构造函数,在 new device 对象时会被调用:
/* src/models/device.js */
/**
* @class device
* @parent Object
* @annotation ["model", "custom_prop"]
*/
export class device {
constructor() {
/**
* @property {Number} pack_type
* @annotation ["readable", "writable", "defvalue:0"]
* 设备类型
*/
this.pack_type = 0;
/**
* @property {Number} pack_params
* @annotation ["readable", "writable", "defvalue:0"]
* 设备参数
*/
this.pack_params = 0;
/**
* @property {Boolean} io1
* @annotation ["readable", "writable", "defvalue:false"]
*/
this.io1 = false;
/**
* @property {Boolean} io2
* @annotation ["readable", "writable", "defvalue:false"]
*/
this.io2 = false;
/**
* @property {Number} a1
* @annotation ["readable", "writable", "defvalue:0"]
*/
this.a1 = 0;
/**
* @property {Number} a2
* @annotation ["readable", "writable", "defvalue:0"]
*/
this.a2 = 0;
/**
* @property {Number} temp
* @annotation ["readable", "writable", "defvalue:0"]
*/
this.temp = 0;
/**
* @property {Number} tps
* @annotation ["readable", "writable", "defvalue:0"]
*/
this.tps = 0;
}
}
# 5.1.2 随机生成设备模型中的数据
在本案例中,为了方便演示,设备模型中的数据都是随机生成的,因此,我们可以修改 device 模型的构造函数,在 new device 对象时随机生成相应的数据,代码如下:
/* src/models/device.js */
export class device {
constructor() { /* device 模型的构造函数 */
/* 属性注释详见上一小节,此处重点展示随机生成数据的代码 */
this.pack_type = Math.round(Math.random() * 1000) % 15;
this.pack_params = Math.round(Math.random() * 1000) % 13;
this.io1 = Math.random() > 0.5;
this.io2 = Math.random() > 0.5;
this.a1 = Math.random() * 1000;
this.a2 = Math.random() * 1000;
this.temp = Math.random() * 1000;
this.tps = Math.round(Math.random() * 1000);
}
}
# 5.2 视图模型设计
完成模型设计后,需要为主界面 home_page 新建一个视图模型 home_page_view_model,步骤详见上文 4.2.1 章节。在 AWTK Designer 中新建并保存视图模型后,会自动在项目目录的"src/view_models"文件夹中生成 home_page_view_model.js 文件。
由于 JS 是脚本语言,可通过名字直接访问对象的成员变量和成员函数,因此视图模型的代码相较于 C 语言简洁很多,MVVM-JS 项目的 ViewModel 框架代码详见 4.2.1 章节。
需要注意的是,在 MVVM-JS 中,View 与 ViewModel 之间通过 ViewModel 的名称来绑定,例如此处的 ViewModel 名称为 home_page_view_model,在界面 UI 文件中可以通过 v-model 属性来指定,代码如下:
<!-- design/default/ui/home_page.xml -->
<window v-model="home_page_view_model" name="home_page" v-on:window_open="{reset}">
....
</window>
在 ViewModel 的 JS 代码文件中则可以通过 ViewModel() 函数来注册,代码如下:
/* src/view_models/home_page_view_model.js */
/**
* @class home_page_view_model
* @parent ViewModel
* @annotation ["model", "view_model", "custom_prop"]
*/
ViewModel('home_page_view_model', {/* home_page_view_model 对象,包含成员属性、成员函数(命令) */});
# 5.2.1 为视图模型增加属性
此处为了实现案例中的相关功能,需要为 home_page_view_model 添加以下属性:
属性名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
items | Array | [] | 设备列表,用于存储device对象 |
unlocked | Boolean | false | 用于解锁 |
currentIndex | Number | -1 | 当前选中设备序号,用于实现插入和删除功能 |
在 AWTK Designer 中添加完成后效果如下图所示:

保存视图模型后,在 home_page_view_model.js 文件中也会将以上属性自动添加到 ViewModel 对象的 data 中,代码如下:
/* src/view_models/home_page_view_model.js */
ViewModel('home_page_view_model', {
data: { /* ViewModel中的成员属性 */
/**
* @property {Array} items
* @annotation ["readable", "writable", "defvalue:[]"]
* 设备列表,用于存储device对象
*/
items: [],
/**
* @property {Boolean} unlocked
* @annotation ["readable", "writable", "defvalue:false"]
* 用于解锁
*/
unlocked: false,
/**
* @property {Number} currentIndex
* @annotation ["readable", "writable", "defvalue:-1"]
* 当前选中设备序号,用于实现插入和删除功能
*/
currentIndex: -1
},
......
});
# 5.2.2 为视图模型增加命令
此处为了实现案例中的相关功能,需要为 home_page_view_model 添加以下命令:
命令名称 | 描述 |
---|---|
setCurrent | 设置当前设备序号 |
insert | 插入设备 |
remove | 移除设备 |
clear | 清空设备列表 |
reset | 重置设备列表 |
在 AWTK Designer 中添加完成后效果如下图所示:

保存视图模型后,在 home_page_view_model.js 文件中也会将以上属性自动添加到 ViewModel 对象的 methods 中,代码如下:
/* src/view_models/home_page_view_model.js */
ViewModel('home_page_view_model', {
methods: {
/**
* @method remove
* @annotation ["command"]
* 移除设备
* @param {String|Object} args 命令的参数。
* @return {TRET} 返回RET_OK表示成功,否则表示失败。
*/
canRemove: function (args) {
return true;
},
remove: function (args) {
return RET_OK;
},
/**
* @method insert
* @annotation ["command"]
* 插入设备
* @param {String|Object} args 命令的参数。
* @return {TRET} 返回RET_OK表示成功,否则表示失败。
*/
canInsert: function (args) {
return true;
},
insert: function (args) {
return RET_OK;
},
/**
* @method clear
* @annotation ["command"]
* 清空设备列表
* @param {String|Object} args 命令的参数。
* @return {TRET} 返回RET_OK表示成功,否则表示失败。
*/
canClear: function (args) {
return true;
},
clear: function (args) {
return RET_OK;
},
/**
* @method reset
* @annotation ["command"]
* 重置设备列表
* @param {String|Object} args 命令的参数。
* @return {TRET} 返回RET_OK表示成功,否则表示失败。
*/
canReset: function (args) {
return true;
},
reset: function (args) {
return RET_OK;
},
/**
* @method setCurrent
* @annotation ["command"]
* 设置当前设备序号
* @param {String|Object} args 命令的参数。
* @return {TRET} 返回RET_OK表示成功,否则表示失败。
*/
canSetCurrent: function (args) {
return true;
},
setCurrent: function (args) {
return RET_OK;
}
},
......
});
接下来,我们逐个实现以上命令的功能。
# 5.2.3 实现设置当前设备序号功能
首先是 setCurrent 命令,该命令始终可用(没有条件限制),用于设置设备列表的当前选中项,即点击列表中某个设备,将它的索引保存到 ViewModel 的 currentIndex 属性中,代码如下:
/* src/view_models/home_page_view_model.js */
ViewModel('home_page_view_model', {
methods: {
canSetCurrent: function (args) {
return true; /* setCurrent 命令始终可用,因此直接返回 true */
},
setCurrent: function (args) { /* 执行命令时传入参数:设备索引(index) */
console.log(args.index); /* 打印设备索引 */
this.currentIndex = args.index; /* 将设备索引保存到 currentIndex 属性 */
this.notifyPropsChanged() /* 通知界面更新 */
return RET_OK;
}
},
......
});
# 5.2.4 实现插入设备功能
insert 命令用于在当前设备列表的选中项前插入新项,该命令同样是只有当选中项索引在设备列表数组中的时候才可用,代码如下:
这里插入的数据是 device 对象,因此需要先导入上文 5.1.1 章节中新增的 device 模型。
/* src/view_models/home_page_view_model.js */
import { device } from "../models/device"; /* 导入 device 模型 */
ViewModel('home_page_view_model', {
methods: {
canInsert: function (args) {
/* 获取当前选中项索引 */
var index = this.currentIndex;
/* remove 命令只有当选中项索引在设备列表数组中的时候才可用 */
return index >= 0 && index <= this.items.length;
},
insert: function (args) {
var index = this.currentIndex; /* 获取当前选中项索引 */
var item = new device; /* 创建 device 对象 */
this.items.splice(index, 0, item); /* 插入数据 */
this.notifyItemsChanged(this.items); /* 通知界面更新 */
return RET_OK;
},
},
......
});
# 5.2.5 实现移除设备功能
remove 命令用于移除当前设备列表中的选中项,该命令只有当选中项索引在设备列表数组中的时候才可用,代码如下:
/* src/view_models/home_page_view_model.js */
ViewModel('home_page_view_model', {
methods: {
canRemove: function (args) {
/* 获取当前选中项索引 */
var index = this.currentIndex;
/* remove 命令只有当选中项索引在设备列表数组中的时候才可用 */
return index >= 0 && index < this.items.length;
},
remove: function (args) {
var index = this.currentIndex; /* 获取当前选中项索引 */
this.items.splice(index, 1); /* 删除设备列表中的选中项 */
this.notifyItemsChanged(this.items); /* 通知界面更新 */
return RET_OK;
},
......
}
});
# 5.2.6 实现清除设备列表功能
clear 命令用于清除设备列表,即删除设备列表中的所有数据,它只有在设备列表中存在数据时可用,代码如下:
/* src/view_models/home_page_view_model.js */
ViewModel('home_page_view_model', {
methods: {
canClear: function (args) {
/* clear 命令只有在设备列表中存在数据时可用 */
return this.items.length > 0;
},
clear: function (args) {
this.items.splice(0, this.items.length); /* 清除设备列表中的所有数据 */
this.notifyItemsChanged(this.items); /* 通知界面更新 */
return RET_OK;
},
},
......
});
# 5.2.7 实现重置设备列表功能
reset 命令用于重置设备列表,此处是向设备列表中添加 50 条数据,该命令只有在设备列表中没有数据时才可用,代码如下:
这里插入的数据是 device 对象,因此需要先导入上文 5.1.1 章节中新增的 device 模型。
/* src/view_models/home_page_view_model.js */
import { device } from "../models/device"; /* 导入 device 模型 */
ViewModel('home_page_view_model', {
methods: {
canReset: function (args) {
/* reset 命令只有在设备列表中没有数据才可用 */
return this.items.length == 0;
},
reset: function (args) {
/* 向设备列表插入50条数据 */
for (var i = 0; i < 50; i++) {
var item = new device; /* 创建 device 对象 */
this.items.splice(this.items.length, 0, item); /* 插入数据 */
}
/* 通知界面更新 */
this.notifyItemsChanged(this.items);
return RET_OK;
},
},
......
});
# 5.3 界面设计
完成 Mvvm-JS-Demo 案例的 Model 和 ViewModel 后,接下来需要进行界面设计。由于 Mvvm-JS-Demo 的界面与 Mvvm-C-Demo 完全一致,因此读者只需参考上文 3.3 章节的内容即可,此处就不过多赘述了。
这里也体现了 MVVM 项目的核心优点:界面与业务逻辑之间的松耦合,它们均可独立变化,二者通过一条条绑定规则进行串联。
# 5.4 运行效果
MVVM-JS 项目的运行过程跟 MVVM-C 项目略有不同,具体详见下文。
# 5.4.1 打包资源
在 AWTK Deisgner 中,打包 MVVM-JS 项目资源的步骤与 MVVM-C 项目一样,点击菜单栏中的"打包"按钮即可,如下图所示:

但 AWTK Designer 在打包 MVVM-JS 项目的资源时,会将 src 下的所有 JS 脚本打包成一个文件,放在资源目录的 scripts 文件夹中,名称为 app.js。
例如此处会将 Mvvm-JS-Demo/src 文件夹中的 JS 文件打包为 res/assets/default/raw/scripts/app.js。在项目运行时,将直接读取这个 app.js 文件来执行里面的函数。
# 5.4.2 模拟运行
A0WTK Designer 内置了 MVVM-JS 项目的模拟程序,因此,MVVM-JS 项目无需编译即可查看效果,如下图所示,点击菜单栏中的"模拟运行":

运行效果如下图所示:

# 5.4.3 编译运行
MVVM-JS 项目的业务逻辑由 JS 脚本实现,程序的 main() 函数只需完成 AWTK、AWTK-MVVM 和 JerryScript 的初始化工作即可,因此实际上 MVVM-JS 项目的启动程序代码是一样的,详见项目的 scr/main.c 和 src/application.c 文件,它们由 AWTK Designer 自动生成。
由于以上特性,AWTK Designer 中不支持直接编译 MVVM-JS 项目,而是内置了一个模拟程序,即 MVVM-JS 项目的启动程序(runFlowAWTK),用来模拟运行 MVVM-JS 项目,详见上一小节。
用户如果想自行编译 MVVM-JS 项目,在项目目录下打开终端,执行 scons 命令即可,如下图所示:

编译成功后,可在项目中看到的 bin/demo.exe 文件(此处以 Windows 平台为例),双击运行的效果与模拟运行一致,详见上一小节中的效果图。
如果 MVVM-JS 项目中使用了 AWFlow Designer 设计的流图(即开发AWTK+AWFlow 低代码应用程序),则必须使用 AWTK Designer 中的模拟运行(runFlowAWTK)查看效果,使用 scons 命令自行编译的程序默认关闭流图功能,关于 AWTK+AWFlow 低代码应用程序的相关介绍请参考:《AWFlow+AWTK低代码应用开发指南》。