Skip to content

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_typeNumber设备类型由于MVVM-JS项目暂不支持自定义枚举类型,此处使用 Number 类型代替
pack_paramsNumber设备参数由于MVVM-JS项目暂不支持自定义枚举类型,此处使用 Number 类型代替
io1Boolean设备数据随机生成,由设备类型决定是否显示
io1Boolean设备数据随机生成,由设备类型决定是否显示
a1Number设备数据随机生成,由设备类型决定是否显示
a2Number设备数据随机生成,由设备类型决定是否显示
tempNumber设备数据随机生成,由设备类型决定是否显示
tpsNumber设备数据随机生成,由设备类型决定是否显示

由于 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 对象时会被调用:

js
/* 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 对象时随机生成相应的数据,代码如下:

js
/* 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 属性来指定,代码如下:

xml
<!-- 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() 函数来注册,代码如下:

js
/* 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 添加以下属性:

属性名称类型默认值描述
itemsArray[]设备列表,用于存储device对象
unlockedBooleanfalse用于解锁
currentIndexNumber-1当前选中设备序号,用于实现插入和删除功能

在 AWTK Designer 中添加完成后效果如下图所示:

保存视图模型后,在 home_page_view_model.js 文件中也会将以上属性自动添加到 ViewModel 对象的 data 中,代码如下:

js
/* 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 中,代码如下:

js
/* 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 属性中,代码如下:

js
/* 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 模型。

js
/* 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 命令用于移除当前设备列表中的选中项,该命令只有当选中项索引在设备列表数组中的时候才可用,代码如下:

js
/* 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 命令用于清除设备列表,即删除设备列表中的所有数据,它只有在设备列表中存在数据时可用,代码如下:

js
/* 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 模型。

js
/* 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低代码应用开发指南》。