# 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 章节,效果如下图所示:

图5.1 新增设备模型
图5.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 中添加完成后效果如下图所示:

图5.2 home_page_view_model中的属性
图5.2 home_page_view_model中的属性

保存视图模型后,在 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 中添加完成后效果如下图所示:

图5.3 home_page_view_model中的命令
图5.3 home_page_view_model中的命令

保存视图模型后,在 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 项目一样,点击菜单栏中的"打包"按钮即可,如下图所示:

图5.4 打包资源
图5.4 打包资源

但 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.5 模拟运行
图5.5 模拟运行

运行效果如下图所示:

图5.6 Mvvm-JS-Demo
图5.6 Mvvm-JS-Demo

# 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 命令即可,如下图所示:

图5.7 编译Mvvm-JS-Demo
图5.7 编译Mvvm-JS-Demo

编译成功后,可在项目中看到的 bin/demo.exe 文件(此处以 Windows 平台为例),双击运行的效果与模拟运行一致,详见上一小节中的效果图。

如果 MVVM-JS 项目中使用了 AWFlow Designer 设计的流图(即开发AWTK+AWFlow 低代码应用程序),则必须使用 AWTK Designer 中的模拟运行(runFlowAWTK)查看效果,使用 scons 命令自行编译的程序默认关闭流图功能,关于 AWTK+AWFlow 低代码应用程序的相关介绍请参考:《AWFlow+AWTK低代码应用开发指南》。