Skip to content

Latest commit

 

History

History
508 lines (388 loc) · 13.9 KB

13.js_model.md

File metadata and controls

508 lines (388 loc) · 13.9 KB

第 13 章 用 JS 语言实现 Model

13.1 介绍

前面我们介绍了用 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 是最好的开发语言。

13.2 用 JS 实现 ViewModel

为了让 View 顺利的与 Model 绑定到一起,用 JS 实现 Model 时需要遵循一些规则。

13.2.1 名称的约定

13.2.1.1 文件名

在 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文件的集合。

13.2.1.2 ViewModel 的名称

ViewModel 的名称通过全局函数 ViewModel() 的第一个参数指定,要求与 XML 的界面描述文件中的 v-model 属性相同,且通常与文件名保持一致。

比如在 temperature.js 中实现 ViewModel,那么 ViewModel() 函数的第一个参数就是 "temperature"。如:

ViewModel('temperature', {
});
13.2.1.2 ViewModel 的名称

13.2.2 ViewModel 的定义

MVVM 中通过一个全局函数 ViewModel() 来实现 ViewModel 的定义,该函数的第一个参数表示 ViewModel 的名称,第二个参数则是 ViewModel 的具体定义,接下来将详细介绍如何实现。

命名建议使用 JS 最流行的驼峰命名方式,首单词的首字母小写,其它单词的首字母大写。 不做强制性要求,但遵循同样的规则可以带来更好的一致性。

13.2.2.1 ViewModel 的数据

数据分为"data"类型和"couputed"类型。

  1. "data"类型的数据对象

通常直接使用或者赋值的数据可以定义在 data 对象中。

比如 ViewModel 有一个名为 value 的数据,其初始化时为 20,代码如下:

ViewModel('temperature', {
  data: {
    value: 20
  }
});

如果 ViewModel 还有一个名为 applydValue 的数据,其初始化值也为 20,代码如下:

ViewModel('temperature_ex', {
  data: {
    value: 20,
    applydValue: 20
  }
});
  1. "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;
    },
    ...
  }
});
13.2.2.2 ViewModel 的命令

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();
    }
  }

可用性检查函数是可选的,如果该函数不存在,则认为该命令始终是可以执行的。

13.2.3 ViewModel 变化事件

当 ViewModel 有变化时,需要通知 View 进行更新。有以下几种方式通知 View:

  1. 通过返回值通知
  • 命令返回 RET_OBJECT_CHANGED 表示 ViewModel 有数据发生变化,需要 View 更新。

如:

ViewModel('temperature_ex', {
  methods: {
    apply: function(args) {
      this.applydValue = this.value;
      return RET_OBJECT_CHANGED;
    }
  }
});
  1. 调用接口通知
  • 调用 notifyPropsChanged() 通知 View 有数据发生变化。

  • 调用 notifyItemsChanged() 通知 View 增加或删除了指定数据集合中的项目,该接口有一个 target 的可选参数,可以设置发生变化的数据集合,为空时默认为 Model 本身。

ViewModel('temperature_ex', {
  methods: {
    apply: function(args) {
      this.applydValue = this.value;
      this.notifyPropsChanged();
    }
  }
});

如果 ViewModel 的变化不是由命令触发的,而是由后台的定时器或者线程触发的,那就只能通过调用接口进行通知。

13.3 用 JS 实现普通 Model

13.3.1 Model 的定义

普通 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 脚本。

13.3.2 Model 变化事件

当 Model 有变化时,需要通知 View 进行更新。有以下几种方式通知 View:

  1. 获取 ViewModel 实例后调用 ViewModel 的通知接口
  • 调用全局函数 getViewModels() 获取指定 ViewModel 实例(数组)。

ViewModel 的通知接口请参阅 13.2.3.2 章节。

  1. 调用全局的通知接口
  • 调用全局函数 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);
    }
  }
});

13.4 用 JS 实现数据格式转换器

用 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 文件中。

13.5 用 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 文件中。

13.6 程序的生命周期

如果需要在程序启动或者退出时执行一些操作,可以使用 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  ===============');
  }
});