node package manager
Don’t reinvent the wheel. Reuse code within your team. Create a free org »

mas-js-sdk

MAS · JavaScript 使用指南

1. 简介

MAS(MIS Application Service)提供了快速开发MIS的多项功能,旨在开发MIS系统时减少与后端的联调沟通,让前端能够轻松HOLD住一个完整MIS的开发。下面我们就MAS最重要的数据存储功能与传统数据库的对比来简要介绍一下MAS的特点。

在传统数据库中,当我们要进行向Todo表中一条数据的增加,我们会这样做:

INSERT INTO Todo (title,content) VALUES ('周会''周二下午2点整');

那么当我们使用MAS的数据存储功能时,实现代码如下:

var todo = MAS.Object.new('Todo');
todo.set('title','周会');
todo.set('content','周二下午2点整');
todo.save().then(function(){
    // success handle 
}).catch(function(){
    // error handle 
})

使用MAS的特点在于:

  • 不需要单独的维护表结构。比如需要新增字段你只需要这样改动代码
var todo = MAS.Object.new('Todo');
todo.set('title','周会');
todo.set('content','周二下午2点整');
todo.set('location', '全民直播6楼会议室');
todo.save().then(function(todo){
    // success handle 
}).catch(function(){
    // error handle 
})
  • Schema Free,数据可以随用随加
  • 能够提供一套统一的SDK,给予不同语言和不同环境的支持

MAS与传统数据库的区别在于:

  1. Schema Free/Not free 的差异;
  2. 数据接口上,MAS 是面向对象的(数据操作接口都是基于 Object 的),开放的(所有移动端都可以直接访问),DB 是面向结构的,封闭的(一般在 Server 内部访问)

目前,MAS针对MIS开发的特点集成了常用的功能:

  1. 数据存储
  2. ACL
  3. 缓存
  4. HTTP跨域代理
  5. 邮件
  6. 短信
  7. 文件上传
  8. 报表服务

接下来我们会一一介绍各个功能的使用

2. SDK安装

对于浏览器环境,只需要引入对应的sdk即可

<script src="./mas.js"></script>

对于node环境来说,你可以使用npm进行安装

npm install mas-js-sdk --save

之后我们就可以进行MAS的使用了

// 浏览器环境 
var appId = 'your appId'
var appKey = 'your appKey'
window.MAS = require('MAS')(appId, appKey );
 
// node环境 
var appId = 'your appId'
var appKey = 'your appKey'
req.__MAS__ = require('mas-js-sdk')(appId, appKey );

3. 安全验证

MAS的安全验证有一套严格的流程,通过这个流程我们可以将数据权限细化到一个数据的读写,其流程步骤为:

  1. 对app的访问权限验证(通过应用创建的appId及appKey进行验证)
  2. 对用户进行权限验证 (通过MAS.User对象login后获得的uid和token进行验证)
  3. 对CRUD的Class进行权限验证(判断用户是否在Class的CRUD相关权限列表中)
  4. 对数据的read和write进行权限验证(通过创建数据时的ACL进行验证)

4. 数据存储

4.1 对象

MAS.Object是MAS对数据存储过程的复杂封装,每个MAS.Object的实例包含了诸多的键值对(key-value)。属性的值严格与JSON方式兼容的数据。当数据进行保存时,MAS.Object会对数据进行JSON.stringify。这个数据是无模式化的(Schema Free),这意味着你不需要提前标注每个对象有哪些key,你只需要随意的添加它就好了,服务器会按照相关逻辑保存它(注意:如果在MAS平台上,没有对添加字段进行相关Class列的添加,那么在查询时会被自动的过滤掉)。

4.1.1 数据类型

MAS.Object支持部分标准的JS数据类型,如下:

var todo = MAS.Object.new('Todo');
 
var number = 2014;
var string = 'famous film name is ' + number;
var date = new Date();
var array = [string, number];
var object = { number: number, string: string };
 
todo.set('testNumber', number);
todo.set('testString', string);
todo.set('testDate', date);
todo.set('testArray', array);
todo.set('testObject', object);
todo.set('testNull', null);
todo.save().then(function(todo) {
  // success 
}, function() {
  // fail 
});

注意,MAS.Object不能存储二进制数据(例如Blob相关),如果要对Blob数据进行存储,那么请使用 MAS.File

4.1.2 创建对象

创建对象可以使用两种方式:

// 1.创建Class后再进行实例化 
var Todo = MAS.Object.extend('Todo');
var todo = new Todo();
 
// 2.直接实例化 
var todo = MAS.Object.new('Todo');

但需要注意的是,不管是extend还是new,对应的参数都应该准确对应你创建应用的Class名称。

4.1.3 保存对象

我们假设已经创建了一个叫做Todo的Class,并且其包含了title、content、location三个自定义列,那么当需要新建一条数据时对应的代码如下:

var todo = MAS.Object.new('Todo');
todo.set('title','new title');
todo.set('content','new content');
todo.set('location','new location');
todo.save().then(function(todo){
    // success 
}).catch(function(){
    // error 
});

为提高代码的可读性,我们建议使用驼峰式命名法(CamelCase)为Class及属性进行命名。类使用大驼峰方式,如UserDetail,属性使用小驼峰,如updatedAt。

此外,在保存对象时我们可以进行fetchWhenSave的设定,fetchWhenSave用于对象成功保存后,自动返回本地已改动属性在云端的最新值,而不是本地save的数据,其默认为false,我们会在更新数据时讲解它的使用场景。

4.1.4 获取对象

每个被保存在服务端的数据都会有一个objectId标示,我们可以通过objectId获得对应的数据:

 var query = new MAS.Query('Todo');
 query.get('57328ca079bc44005c2472d0').then(function (todo) {
   // success 
   // data 就是 objectId 为 57328ca079bc44005c2472d0 的 Todo 对象实例 
 }).catch(function () {
   // error 
 });

如果不想使用查询,还可以通过从本地构建一个 id,然后调用接口从云端把这个 id 的数据拉取到本地,示例代码如下:

var todo = MAS.Object.new('Todo');
 
todo.setId('57328ca079bc44005c2472d0');
// or 
todo.set('objectId','57328ca079bc44005c2472d0');
 
todo.fetch().then(function(todo){
    // success 
    var title = todo.get('title');// 读取 title 
    var content = todo.get('content');// 读取 content 
}).catch(function(){
    // error 
});
4.1.5 获取objectId

每一次对象存储成功之后,云端都会返回 objectId,它是一个Class中全局唯一的属性。

var todo = MAS.Object.new('Todo');
todo.set('title','new title');
todo.set('content','new content');
todo.set('location','new location');
todo.save().then(function(todo){
    // success 
    var objectId = todo.getId();
    // or 
    objectId = todo.get('objectId');
    
}).catch(function(){
    // error 
});
4.1.6 访问对象属性

访问Todo的对象属性的方法为:

var todo = MAS.Object.new('Todo');
todo.setId('57328ca079bc44005c2472d0');
todo.fetch().then(function(todo){
    // success 
    var objectId = todo.getId();
    var acl = todo.getACL();
    var title = todo.get('title');
    var content = todo.get('content');
    var createdAt = todo.get('createdAt');
    var updatedAt = todo.get('updatedAt');
}).catch(function(){
    // error 
});

如果访问了并不存在的属性,SDK 并不会抛出异常,而是会返回空值。

4.1.7 默认属性

MAS创建Class会有对应的默认属性,它包括了objectId、createdUid、updatedUid、createdAt、updatedAt。

  • objectId:Class中数据的全局唯一标示,相当于关系型数据库中的主键。
  • createdUid:创建当条数据的用户Id
  • updatedUid:修改数据的用户Id
  • createdAt:创建数据的时间,Unix时间戳
  • updatedAt:修改数据的时间,Unix时间戳
4.1.8 同步对象

多终端共享一个数据时,为了确保当前客户端拿到的对象数据是最新的,可以调用刷新接口来确保本地数据与云端的同步:

 // 使用已知 objectId 构建一个 MAS.Object 
 var todo = new Todo();
 todo.setId('5590cdfde4b00f7adb5860c8');
 todo.fetch().then(function (todo) {
   // todo 是从服务器加载到本地的 Todo 对象 
   var objectId = todo.getId();
 }).catch(function (error) {
 
 });
4.1.9 更新对象

MAS 上的更新对象都是针对单个对象,云端会根据 有没有 objectId 来决定是新增还是更新一个对象。

 // 使用已知 objectId 构建一个 MAS.Object 
var todo = new Todo();
todo.setId('5590cdfde4b00f7adb5860c8');
todo.fetch().then(function (todo) {
    // todo 是从服务器加载到本地的 Todo 对象 
    todo.set('title', '需求临时变更通知');
    todo.set('content', '需求被产品汪变更了,我们需要改时间');
    
    // 更新了服务端objectId为5590cdfde4b00f7adb5860c8的title和content字段 
    return todo.save();
}).then(function(todo){
    var title = todo.get('title'); // title = 需求临时变更通知 
    var content = todo.get('content'); // content = 需求被产品汪变更了,我们需要改时间 
}).catch(function(){
});

更新操作是覆盖式的,云端会根据最后一次提交到服务器的有效请求来更新数据。更新是字段级别的操作,未更新的字段不会产生变动,这一点请不用担心。

由于更新会根据最后一次提交到服务器的请求来判断(乐观锁机制),因此为了保证在多人同时修改同一条数据时,你可以使用fetchWhenSave保证数据与服务端的同步,MAS.Object的fetchWhenSave默认为false。

考虑这样一个场景:一篇 wiki 文章允许任何人来修改,它的数据表字段有:content(wiki 内容)、version(版本号)。每当 wiki 内容被更新后,其 version 也需要更新(+1)。用户 A 要修改这篇 wiki,从数据表中取出时其 version 值为 3,当用户 A 完成编辑要保存新内容时,如果数据表中的 version 仍为 3,表明这段时间没有其他用户更新过这篇 wiki,可以放心保存;如果不是 3而是更高的值,那么此次修改应该被丢弃,当设置了fetchWhenSave为true时,客户端将会得到最新的修改值,保证了数据的同步(fetchWhenSave的依据逻辑为updatedAt字段)。

new MAS.Query('Wiki').first().then(function (wiki) {
    var currentVersion = wiki.get('version');
    wiki.fetchWhenSave(true);
    wiki.set('version', currentVersion + 1);
    return wiki.save();
}).then(function (wiki) {
    // 保存成功,version为最后一次修改的version 
}).catch(function (error) {
    // 异常处理 
});
4.1.10 数值更新

对Number数值的更新MAS提供了increment方法

 var todo = MAS.Object.new('Todo');
 todo.setId('57328ca079bc44005c2472d0');
 todo.set('views', 0);
 todo.save().then(function (todo) {
   todo.increment('views', 1);
   todo.fetchWhenSave(true);
   return todo.save();
 }).then(function (todo) {
   // 使用了 fetchWhenSave 选项,save 成功之后即可得到最新的 views 值 
 }).catch(function(){
 
 });
4.1.11 更新数组

更新数组是原子操作。使用以下方法可以方便地维护数组类型的数据:

  • MAS.Object.prototype.add(attrKey, value) 将指定对象附加到数组末尾。
  • MAS.Object.addUnique(attrKey, value) 如果数组中不包含指定对象,将该对象加入数组末尾
  • MAS.Object.remove(attrKey, value) 从数组字段中删除指定对象的所有实例

例如,Todo 对象有一个提醒时间 reminders 字段,是一个数组,代表这个日程会在哪些时间点提醒用户。比如有个拖延症患者把闹钟设为早上的 7:10、7:20、7:30:

 var reminder1 = +new Date('2015-11-11 07:10:00');
 var reminder2 = +new Date('2015-11-11 07:20:00');
 var reminder3 = +new Date('2015-11-11 07:30:00');
 
 var reminders = [reminder1, reminder2, reminder3];
 
 var todo = MAS.Object.new('Todo');
 // 指定 reminders 是一个 Unix时间戳 对象数组 
 todo.addUnique('reminders', reminders);
 todo.save().then(function (todo) {
   console.log(todo.get('reminders')); // equalTo reminders 
 }).catch(function () {
   // 异常处理 
 });
4.1.12 删除对象

假如某一个 Todo 完成了,用户想要删除这个 Todo 对象,可以如下操作:

var todo = MAS.Object.new('Todo');
todo.setId('57328ca079bc44005c2472d0');
todo.destroy().then(function () {
  // 删除成功 
}, function () {
  // 删除失败 
});

删除对象是一个较为敏感的操作。在控制台创建对象的时候,请认真考虑Class对应的权限设置,对于数据的删除,我们推荐定义一个字段isDeleted,依靠isDeleted的值来判断数据是否被删除的方式。

4.1.12 批量操作

为了减少网络交互的次数太多带来的时间浪费,你可以在一个请求中对多个对象进行创建、更新、删除、获取。接口都在 MAS.Object 这个类下面:

var objects = []; // 构建一个本地的 MAS.Object 对象数组 
 
 // 批量创建(更新) 
MAS.Object.saveAll(objects).then(function (objects) {
  // 成功 
}).catch(function () {
  // 异常处理 
});
 
// 批量删除 
MAS.Object.destroyAll(objects).then(function () {
  // 成功 
}).catch(function () {
  // 异常处理 
});
 
// 批量获取 
MAS.Object.fetchAll(objects).then(function (objects) {
  // 成功 
}).catch(function () {
  // 异常处理 
});

批量设置 Todo 已经完成:

var query = new MAS.Query('Todo');
query.find().then(function (todos) {
  todos.forEach(function(todo) {
    todo['status'] = 1;
  });
  return MAS.Object.saveAll(todos);
}).then(function(todos) {
  // 更新成功 
}).catch(function () {
  // 异常处理 
});

不同类型的批量操作所引发不同数量的 API 调用,假设对象数量为n,fetchAll及saveAll发送n个请求,destroyAll发送1个请求。

4.2 查询

MAS.Query 是构建针对 MAS.Object 查询的基础类。每次查询默认最多返回 10 条符合条件的结果,要更改这一数值,需要使用到limit方法。

4.2.1 创建查询
var query = new MAS.Query('Todo');
4.2.2 根据objectId进行查询
 var query = new MAS.Query('Todo');
 query.get('57328ca079bc44005c2472d0').then(function (todo) {
   // success 
   // data 就是 objectId 为 57328ca079bc44005c2472d0 的 Todo 对象实例 
 }).catch(function () {
   // error 
 });
4.2.3 条件查询

根据不同条件来过滤结果,比如查询最迫切需要完成的日程列表 Todo,此时基于 priority 构建一个查询就可以得到符合条件的对象:

var query = new MAS.Query('Todo');
// 查询 priority 是 0 的 Todo 
query.equalTo('priority', 0);
query.find().then(function (results) {
    var priorityEqualsZeroTodos = results;
}).catch(function () {
 
});

每次查询默认最多返回 10条符合条件的结果,要更改这一数值,需要使用limit方法。 将以上逻辑用 SQL 语句表达:

SELECT * FROM Todo WHERE priority = 0

当多个查询条件并存时,它们之间默认为 AND 关系,即查询只返回满足了全部条件的结果。建立 OR 关系则需要使用 MAS.Query.or方法。

请注意,在简单查询中,如果对一个对象的同一属性设置多个条件,那么先前的条件会被覆盖,查询只返回满足最后一个条件的结果。例如,我们要找出优先级为 0 和 1 的所有 Todo,错误写法是:

 var query = new MAS.Query('Todo');
 query.equalTo('priority', 0);
 query.equalTo('priority', 1);
 query.find().then(function (results) {
 // 如果这样写,第二个条件将覆盖第一个条件,查询只会返回 priority = 1 的结果 
 }).catch(function () {
 
 });

正确作法是使用 OR 关系 来构建条件。

4.2.3.1 比较查询
  1. 等于:equalTo
  2. 不等于: notEqualTo
  3. 大于:greaterThan
  4. 大于等于:greaterThanOrEqualTo
  5. 小于:lessThan
  6. 小于等于:lessThanOrEqualTo

利用上述介绍的逻辑操作的接口,我们可以很快地构建条件查询。

例如,查询优先级小于 2 的所有 Todo :

var query = new MAS.Query('Todo');
query.lessThan('priority', 2);

要查询优先级大于等于 2 的 Todo:

query.greaterThanOrEqualTo('priority',2);
4.2.3.2 正则匹配查询

正则匹配查询是指在查询条件中使用正则表达式来匹配数据,查询指定的 key 对应的 value 符合正则表达式的所有对象。 例如,要查询标题包含中文的 Todo 对象可以使用如下代码:

var query = new MAS.Query('Todo');
var regExp = new RegExp('[\u4e00-\u9fa5]', 'i');
query.matches('title', regExp);
query.find().then(function (results) {
 
}).catch(function () {
 
});

正则匹配查询只适用于字符串类型的数据。

4.2.3.3 包含查询

包含查询类似于传统 SQL 语句里面的 LIKE %keyword% 的查询,比如查询标题包含「龙神」的 Todo:

query.contains('title','龙神');

翻译成 SQL 语句就是:

SELECT * FROM Todo WHERE title LIKE '%龙神%'

不包含查询与包含查询是对立的,不包含指定关键字的查询,可以使用 正则匹配方法 来实现。例如,查询标题不包含「机票」的 Todo,正则表达式为 ^((?!机票).)*$:

var query = new MAS.Query('Todo');
var regExp = new RegExp('^((?!机票).)*#39;, 'i');
query.matches('title', regExp);

但是基于正则的模糊查询有两个缺点:

  • 当数据量逐步增大后,查询效率将越来越低
  • 没有文本相关性排序

还有一个接口可以精确匹配不等于,比如查询标题不等于「出差、休假」的 Todo 对象:

var query = new MAS.Query('Todo');
var filterArray = ['出差', '休假'];
query.notContainedIn('title', filterArray);
4.2.3.3 数组查询

当一个对象有一个属性是数组的时候,针对数组的元数据查询可以有多种方式。例如,在 数组 一节中我们为 Todo 设置了 reminders 属性,它就是一个日期数组,现在我们需要查询所有在 8:30 会响起闹钟的 Todo 对象:

var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00')];
query.containsAll('reminders', reminderFilter);
 
// 也可以使用 equals 接口实现这一需求 
var targetDateTime = +new Date('2015-11-11 08:30:00');
query.equalTo('reminders', targetDateTime);

如果你要查询精确匹配 8:30、9:30 这两个时间点响起闹钟的 Todo,可以使用如下代码:

var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00'), +new Date('2015-11-11 09:30:00')];
query.containsAll('reminders', reminderFilter);

注意这里是精确关系,假如有一个 Todo 会在 8:30、9:30、10:30 响起闹钟,它不会被查询出来的。

如果要使用类似于SQL的IN操作,那么可以使用 containedIn 和 notContainedIn :

var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00'), +new Date('2015-11-11 09:30:00')];
query.containedIn('reminders', reminderFilter);

这里变为了包含关系,假如有一个 Todo 会在 8:30、9:30、10:30 响起闹钟,它会被查询出来的。

4.2.3.4 字符串匹配

使用 startsWith 可以过滤出以特定字符串开头的结果,这有点像 SQL 的 LIKE 条件。因为支持索引,所以该操作对于大数据集也很高效。

// 找出开头是「早餐」的 Todo 
var query = new MAS.Query('Todo');
query.startsWith('content', '早餐');

另外你也可以使用endWith,但它与matches一样不支持索引:

// 找出结尾是「早餐」的 Todo 
var query = new MAS.Query('Todo');
query.endWith('content', '早餐');
4.2.3.5 OR查询

OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级是大于等于 3 或者已经完成了的 Todo:

var priorityQuery = new MAS.Query('Todo');
priorityQuery.greaterThanOrEqualTo('priority', 3);
 
var statusQuery = new MAS.Query('Todo');
statusQuery.equalTo('status', 1);
 
var query = MAS.Query.or(priorityQuery, statusQuery);
// 返回 priority 大于等于 3 或 status 等于 1 的 Todo 
4.2.3.6 查询结果

例如很多应用场景下,只要获取满足条件的一个结果即可,例如获取满足条件的第一条 Todo:

var query = new MAS.Query('Comment');
query.equalTo('priority', 0);
query.first().then(function (data) {
  // data 就是符合条件的第一个 MAS.Object 
}).catch(function (error) {
 
});

为了防止查询出来的结果过大,云端默认针对查询结果有一个数量限制,即 limit,它的默认值是 10。比如一个查询会得到 10000 个对象,那么一次查询只会返回符合条件的 100 个结果。limit 允许取值范围是 1 ~ Number.MAV_VALUE。例如设置返回 10 条结果:

var query = new MAS.Query('Todo');
var now = +new Date();
query.lessThanOrEqualTo('createdAt', now);//查询今天之前创建的 Todo 
query.limit(100);// 最多返回 100 条结果 

注意,我们不太建议设定太大的limit,这样会导致数据查询及传输很慢致使压垮数据库。

设置 skip 这个参数可以告知云端本次查询要跳过多少个结果。将 skip 与 limit 搭配使用可以实现翻页效果,这在客户端做列表展现时,尤其在数据量庞大的情况下就使用技术。例如,在翻页中,一页显示的数量是 10 个,要获取第 3 页的对象:

var query = new MAS.Query('Todo');
var now = +new Date();
query.lessThanOrEqualTo('createdAt', now);//查询今天之前创建的 Todo 
query.limit(100);// 最多返回 10 条结果 
query.skip(20);// 跳过 20 条结果 

通常列表展现的时候并不是需要展现某一个对象的所有属性,例如,Todo 这个对象列表展现的时候,我们一般展现的是 title 以及 content,我们在设置查询的时候,也可以告知云端需要返回的属性有哪些,这样既满足需求又节省了流量,也可以提高一部分的性能,代码如下:

var query = new MAS.Query('Todo');
query.select('title', 'content');
query.first().then(function (todo) {
  console.log(todo.get('title')); // √ 
  console.log(todo.get('content')); // √ 
  console.log(todo.get('location')); // undefined 
}).catch(function (error) {
  // 异常处理 
});
4.2.3.7 统计总数

通常用户在执行完搜索后,结果页面总会显示出诸如「搜索到符合条件的结果有 1020 条」这样的信息。例如,查询一下今天一共完成了多少条 Todo:

var query = new MAS.Query('Todo');
query.equalTo('status', 1);
query.count().then(function (count) {
    console.log(count);
}).catch(function (error) {
 
});
4.2.3.8 排序

对于数字、字符串、日期类型的数据,可对其进行升序或降序排列。

// 按时间,升序排列 
query.addAscending('createdAt');
 
// 按时间,降序排列 
query.addDescending('createdAt');

一个查询可以附加多个排序条件,如按 priority 升序、createdAt 降序排列:

var query = new MAS.Query('Todo');
query.ascending('priority');
query.descending('createdAt');
4.2.3.9 查询性能优化

影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。

  • 不等于和不包含查询(无法使用索引)
  • 通配符在前面的字符串查询(无法使用索引)
  • 有条件的 count(需要扫描所有数据)
  • skip 跳过较多的行数(相当于需要先查出被跳过的那些行)
  • 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引)
  • 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据)

4.3 用户

用户系统几乎是每款应用都要加入的功能。除了基本的注册、登录和密码重置,甚至还会使用手机号一键登录、短信验证码登录等功能。

MAS.User 是用来描述一个用户的特殊对象,它是 MAS.Object的子类 ,与之相关的数据都保存在 _User 数据表中,其默认fetchWhenSave为true。

4.3.1 用户的属性

用户名、密码、邮箱及电话是默认提供的四个属性,访问方式如下:

var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {
    var username = user.getUsername();
    var password = user.getPassword();
    var email = user.getEmail();
    var phonenumber = user.getPhonenumber();
 }).catch(function (error) {
 
 });

用户对象和普通对象一样也支持添加自定义属性。例如,为当前用户添加年龄属性:

var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {
    user.set('age', 25);
    return user.save();
 }).catch(function (error) {
 
 });
4.3.2 注册

例如,注册一个用户的示例代码如下(用户名 Tom 密码 cat!@#123):

var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.set('email', '395693101@qq.com');
user.set('phonenumber', '18500742221');
user.resgiter().then(function (user) {
 
 }).catch(function (error) {
 
 });

请注意,MAS并不会加密你的密码,因此你需要自己对密码进行加密处理。

4.3.3 登录
var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {
 
}).catch(function (error) {
 
});
4.3.4 当前用户

开微博或者微信,它不会每次都要求用户都登录,这是因为它将用户数据缓存在了客户端。同样,只要是调用了登录相关的接口,MAS JS SDK 都会自动缓存登录用户的数据。 例如,判断当前用户是否为空,为空就跳转到登录页面让用户登录,如果不为空就跳转到首页:

var currentUser = MAS.User.current;
if (currentUser) {
   // 跳转到首页 
}else {
   //currentUser 为空时,可打开用户注册界面… 
}
4.3.4 SessionToken

所有登录接口调用成功之后,云端会返回一个 SessionToken 给客户端,客户端在发送 HTTP 请求的时候,JavaScript SDK 会在 HTTP 请求里面自动添加上当前用户的 SessionToken 和其objectId 作为这次请求发起者 MAS.User 的身份认证信息。

4.3.5 用户查询

查询用户代码如下:

var query = new MAS.Query(MAS.User);

4.3 角色

角色可以被称为组,其目的是为了将对应的user进行分类,比如:CEO、CTO、运营、技术、产品等。

MAS.Role 是用来描述一个组的特殊对象,它同样是 MAS.Object的子类 ,与之相关的数据都保存在 _Role 数据表中,其默认fetchWhenSave为true。

4.3.1 角色的属性

名称和用户是默认提供的两个属性,访问方式如下:

var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function (role) {
    var name = role.get('name');
    var users = role.getUsers(); // 返回MAS.User的实例的数组 
 }).catch(function (error) {
 
 });
4.3.2 添加用户

调用addUser方法,可以将一个用户添加到角色中:

var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function(r){
    role = r;
    var query = new MAS.Query('_User');
    query.equalTo('username','Tom');
    return query.first();
}).then(function(user){
    // 将Tom用户添加到此角色中 
    role.addUser(user);
    // 保存到数据库中 
    return role.save();
}).then(function(){
    
}).catch(function(){
 
});
4.3.2 删除用户

调用removeUser方法,可以将一个用户从角色中删除:

var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function(r){
    role = r;
    var query = new MAS.Query('_User');
    query.equalTo('username','Tom');
    return query.first();
}).then(function(user){
    // 将Tom用户从此角色中删除 
    role.removeUser(user);
    // 保存到数据库中 
    return role.save();
}).then(function(){
    
}).catch(function(){
 
});
4.3.3 角色查询

查询用户代码如下:

var query = new MAS.Query(MAS.Role);

5. ACL

数据安全在应用开发的任何阶段都应该被重视。因此在这里我们对MAS的ACL记性讨论,如何使用MAS提供的安全功能模块为应用以及数据提供安全保障。

列举一个场景: 假设我们要做一个极简的论坛:用户只能修改或者删除自己发的帖子,其他用户则只能查看。

5.1 基于用户的权限管理

5.1.1 单用户权限设置

以上需求在 MAS 中实现的步骤如下:

  1. 写一篇帖子
  2. 设置帖子的「读」权限为所有人可读。
  3. 设置帖子的「写」权限为作者可写。
  4. 保存帖子

实例代码如下:

 // 新建一个帖子对象 
  var Post = MAS.Object.extend('Post');
  var post = new Post();
  post.set('title', '大家好,我是新人');
 
  // 新建一个 ACL 实例 
  var acl = new MAS.ACL();
  acl.setPublicReadAccess(true);
  acl.setWriteAccess(MAS.User.current, true);
 
  // 将 ACL 实例赋予 Post 对象 
  post.setACL(acl);
  post.save().then(function() {
    // 保存成功 
  }).catch(function() {
  
  });

以上代码产生的效果在 MAS平台的Post 表 可以看到,这条记录的 ACL 列上的值为:

{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true}}

此时,这种 ACL 值的表示:所有用户均有「读」权限,而 objectId 为 55b9df0400b0f6d7efaa8801 拥有「写」权限,其他用户不具备「写」权限。

5.1.2 多用户权限设置

假如需求增加为:帖子的作者允许某个特定的用户可以修改帖子,除此之外的其他人不可修改。 实现步骤就是额外指定一个用户,为他设置帖子的「写」权限:

 // 创建一个针对 User 的查询 
 var query = new MAS.Query('_User');
 query.get('55098d49e4b02ad5826831f6').then(function(otherUser) {
   var post = new MAS.Object('Post');
   post.set('title', '大家好,我是新人');
 
   // 新建一个 ACL 实例 
   var acl = new MAS.ACL();
   acl.setPublicReadAccess(true);
   acl.setWriteAccess(MAS.User.current, true);
   acl.setWriteAccess(otherUser, true);
 
   // 将 ACL 实例赋予 Post 对象 
   post.setACL(acl);
 
   // 保存到云端 
   return post.save();
 }).then(function() {
   // 保存成功 
 }).catch(function() {
 
 });

执行完毕上面的代码,回到MAS系统,可以看到,该条 Post 记录里面的 ACL 列的内容如下:

{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true},"55f1572460b2ce30e8b7afde":{"write":true}}

从结果可以看出,该条 Post 已经允许 Id 为 55b9df0400b0f6d7efaa8801 以及 55f1572460b2ce30e8b7afde 两个用户(MAS.User)可以修改,他们拥有 write:ture 的权限,也就是「写」权限。

基于用户的权限管理比较简单直接,理解起来成本较低。

5.1.3 局限性探讨

再进一步的场景: 论坛升级,需要一个特定的管理员(Administrator)来统一管理论坛的帖子,他可以修改帖子的内容,删除不合适的帖子。

论坛升级之后,用户发布帖子的步骤需要针对上一小节做如下调整:

  1. 写一篇帖子
  2. 设置帖子的「读」权限为所有人。
  3. 设置帖子的「写」权限为作者以及管理员
  4. 保存帖子

我们可以设想一下,每当论坛产生一篇帖子,就得为管理员添加这篇帖子的「写」权限。

假如做权限管理功能的时候都依赖基于用户的权限管理,那么一旦产生变化就会发现这种实现方式的局限性。

比如新增了一个管理员,新的管理员需要针对目前论坛所有的帖子拥有管理员应有的权限,那么我们需要把数据库现有的所有帖子循环一遍,为新的管理员增加「写」权限。

假如论坛又一次升级了,付费会员享有特殊帖子的读权限,那么我们需要在发布新帖子的时候,设置「读」权限给部分人(付费会员)。这需要查询所有付费会员并一一设置。

毫无疑问,这种实现方式是完全失控的,基于用户的权限管理,在针对简单的私密分享类的应用是可行的,但是一旦产生需求变更,这种实现方式是不被推荐的。

5.1.4 基于角色的权限设置

管理员,会员,普通用户这三种概念在程序设计中,被定义为「角色」。 我们可以看出,在列出的需求场景中,「权限」的作用是用来区分某一数据是否允许某种角色的用户进行操作。

「权限」只和「角色」对应,而用户也和「角色」对应,为用户赋予「角色」,然后管理「角色」的权限,完成了权限与用户的解耦。

因此我们来解释 MAS 中「权限」和「角色」的概念。

「权限」在 MAS 服务端只存在两种权限:读、写。 「角色」在 MAS 服务端没有限制,唯一要求的就是在一个应用内,角色的名字唯一即可,至于某一个「角色」在当前应用内对某条数据是否拥有读写的「权限」应该是有开发者的业务逻辑决定,而 MAS 提供了一系列的接口帮助开发者快速实现基于角色的权限管理。

为了方便开发者实现基于角色的权限管理,MAS在 SDK 中集成了一套完整的 ACL (Access Control List) 系统。通俗的解释就是为每一个数据创建一个访问的白名单列表,只有在名单上的用户(MAS.User)或者具有某种角色(MAS.Role)的用户才能被允许访问。

为了更好地保证用户数据安全性, MAS 表中每一张都有一个 ACL 列。当然,MAS 还提供了进一步的读写权限控制。

一个 User 必须拥有读权限(或者属于一个拥有读权限的 Role)才可以获取一个对象的数据,同时,一个 User 需要写权限(或者属于一个拥有写权限的 Role)才可以更改或者删除一个对象。下面列举几种常见的 ACL 使用范例。

5.1.5 ACL 权限管理
5.1.5.1 默认权限

在没有显式指定的情况下,LeanCloud 中的每一个对象都会有一个默认的 ACL 值。这个值代表了所有的用户对这个对象都是可读可写的。此时你可以在数据管理的表中 ACL 属性中看到这样的值:

  {"*":{"read":true,"write":true}}

在 基于用户的权限管理 中,已经在代码里面演示了通过 ACL 来实现基于用户的权限管理,那么基于角色的权限管理也是依赖 ACL 来实现的,只是在介绍详细的操作之前需要介绍「角色」这个重要的概念。

5.1.6 角色的权限管理
5.1.6.1 角色的创建

首先,我们来创建一个 Administrator 的角色。

这里有一个需要特别注意的地方,因为 MAS.Role 本身也是一个 AVObject,它自身也有 ACL 控制,并且它的权限控制应该更严谨,如同「论坛的管理员有权力任命版主,而版主无权任命管理员」一样的道理,所以创建角色的时候需要显式地设定该角色的 ACL,而角色是一种较为稳定的对象:

 // 新建一个角色,并把为当前用户赋予该角色 
 var roleAcl = new MAS.ACL();
 roleAcl.setPublicReadAccess(true);
 roleAcl.setPublicWriteAccess(false);
 
 // 当前用户是该角色的创建者,因此具备对该角色的写权限 
 roleAcl.setWriteAccess(MAS.User.current true);
 
 //新建角色 
 var administratorRole = new MAS.Role('Administrator', roleAcl);
 administratorRole.save().then(function(role) {
   // 创建成功 
 }).catch(function() {
 
 });

执行完毕之后,可以查看 _Role 表里已经存在了一个 Administrator 的角色。 另外需要注意的是:可以直接通过 系统的权限设置 直接设置权限。并且我们要强调的是:

ACL 可以精确到 Class,也可以精确到具体的每一个对象(表中的每一条记录)。

5.1.6.2 为对象设置角色的访问权限

我们现在已经创建了一个有效的角色,接下来为 Post 对象设置 Administrator 的访问「可读可写」的权限,设置成功以后,任何具备 Administrator 角色的用户都可以对 Post 对象进行「可读可写」的操作了:

// 新建一个帖子对象 
 var Post = MAS.Object.extend('Post');
 var post = new Post();
 post.set('title', '大家好,我是新人');
 
 // 新建一个角色,并把为当前用户赋予该角色 
 var administratorRole = new MAS.Role('Administrator');
 
 //为当前用户赋予该角色 
 administratorRole.addUser(MAS.User.current);
 
 //角色保存成功 
 administratorRole.save().then(function(administratorRole) {
   // 新建一个 ACL 实例 
   var acl = new MAS.ACL();
   acl.setPublicReadAccess(true);
   acl.setRoleWriteAccess(administratorRole, true);
 
   // 将 ACL 实例赋予 Post 对象 
   post.setACL(acl);
   return post.save();
 }).then(function(post) {
   // 保存成功 
 }).catch(function() {
 
 });
5.1.6.3 用户角色的赋予和剥夺

经过以上两步,我们还差一个给具体的用户设置角色的操作,这样才可以完整地实现基于角色的权限管理。

在通常情况下,角色和用户之间本是多对多的关系,比如需要把某一个用户提升为某一个版块的版主,亦或者某一个用户被剥夺了版主的权力,以此类推,在应用的版本迭代中,用户的角色都会存在增加或者减少的可能,因此,MAS 也提供了为用户赋予或者剥夺角色的方式。 注意:在代码级别,为角色添加用户 与 为用户赋予角色 实现的代码是一样的。 此类操作的逻辑顺序是:

  • 赋予角色:首先判断该用户是否已经被赋予该角色,如果已经存在则无需添加,如果不存在则将该用户(MAS.User)添加到角色实例中。
// 构建 MAS.Role 的查询 
 var roleQuery = new MAS.Query('_Role');
 roleQuery.equalTo('name', 'Administrator');
 roleQuery.find().then(function(results) {
   if (results.length > 0) {
 
     // 如果角色存在 
     var administratorRole = results[0];
     roleQuery.containedIn('users', MAS.User.current.getId());
     return roleQuery.find();
   } else {
     // 如果角色不存在新建角色 
     var administratorRole = new MAS.Role('Administrator');
     
     //为当前用户赋予该角色 
     administratorRole.addUser(MAS.User.current)
     administratorRole.save();
   }
 }).then(function(userForRole) {
   //该角色存在,但是当前用户未被赋予该角色 
   if (userForRole.length === 0) {
     // 为当前用户赋予该角色 
     var administratorRole = new MAS.Role('Administrator');
     administratorRole.addUser(MAS.User.current)
     administratorRole.save();
   }
 }).catch(function() {
 
 });

角色赋予成功之后,基于角色的权限管理的功能才算完成。

另外,此处不得不提及的就是角色的剥夺:

  • 剥夺角色: 首先判断该用户是否已经被赋予该角色,如果未曾赋予则不做修改,如果已被赋予,则将对应的用户(MAS.User)从该角色中删除。
// 构建 MAS.Role 的查询 
var roleQuery = new MAS.Query('_Role');
roleQuery.equalTo('name', 'Moderator');
roleQuery.find().then(function(results) {
 // 如果角色存在 
 if (results.length > 0) {
   var moderatorRole = results[0];
   roleQuery.containedIn('users', MAS.User.current.getId())
   return roleQuery.find();
 }
}).then(function(userForRole) {
 //该角色存在,并且也拥有该角色 
 if (userForRole.length > 0) {
   // 剥夺角色 
   moderatorRole.removeUser(MAS.User.current());
   return moderatorRole.save();
 }
}).then(function() {
 // 保存成功 
}).catch(function() {
 
});

6. 缓存

MAS.Cache是对Redis的代理,Cache能加快查询,减少数据库的压力,目前Cache能支持大部分的Redis方法(但例如pub和sub是不被允许的),如果不熟悉Redis的API,你可以从这里学习如何使用。

例如我们使用缓存来规避用户反复提交数据,其实现如下:

var MAX_SUBMIT_COUNT = 20;
var objectId = MAS.User.current.getId();
var key = 'qmtv_cache_' + objectId;
MAS.Cache.command('get ' + key).then(function(body){
    var count = 0;
    if(body.data && body.data.result){
        count = Number(body.data.result);
    }
    
    if(count >= 20){
        return Promise.reject(new Error('submit too often'));
    }
    
    count += 1;
    return MAS.Cache.command('set ' + key + ' ' + count);
}).then(function(){
    return MAS.Cache.command('expire ' + key + ' 3600');
}).then(function(){
    // 其余逻辑 
}).catch(function(){
    // 拒绝此次提交 
});

7. HTTP请求代理

在开发过程中,我们可能会对一些接口进行HTTP请求,例如请求PC主站的主播列表,或者说我们需要去调用后端的Service接口。一般情况下我们可以通过CORS来进行跨域,但是为了安全起见,PC主站或一些Service接口只允许特定域下的CORS,这个时候我们就可以使用HTTP请求代理功能方便的获得这些数据,例如:

 
MAS.HttpProxy.send(
    "GET",
    'http://www.quanmin.tv'
).then(function () {
    done();
});
 
// application/www-form-urlencode 
MAS.HttpProxy.send(
    "POST",
    'http://www.quanmin.tv/homeapi/rank',
    {p: {}},
    {"Content-Type": "application/json"}
).then(function () {
   done();
});
 
// multipart/form-data 
MAS.HttpProxy.send(
    "POST",
    'http://www.quanmin.tv/homeapi/rank',
    {p: {}},
    null,
    __dirname + '/conf.js'
).then(function () {
   done();
});

注意,在node环境中,文件只需要是一个文件路径即可,但在浏览器环境中,需要传入file对象。

8. 邮件

 
// 单发邮件 
MAS.Mail(
    "395693101@qq.com", // from 
    "395693101@qq.com", // to 
    "test1", // subject 
    "hello world!" // html 
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});
 
// 群发邮件 
MAS.Mail(
    "395693101@qq.com",
    ["395693101@qq.com", "395693102@qq.com"],
    "test1",
    "hello world!"
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});
 
// 携带文件 
MAS.Mail(
    "395693101@qq.com",
    "395693101@qq.com",
    "test2",
    "hello world!",
    __dirname + '/conf.js'
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});

注意,在node环境中,文件只需要是一个文件路径即可,但在浏览器环境中,需要传入file对象。

9. 短信

// 单发短信 
MAS.SMS(
    '18500742221', 
    'just a test! num 0!'
).then(function (res) {
   if (res.body && res.body.error == 0) {
       done();
   }
});
 
// 群发短信 
MAS.SMS(
    ['18500742221', '17011964287'], 
    'just a test! num 1!'
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});

10. 文件上传

MAS.UploadFile(__dirname + '/conf.js').then(function (res) {
    if (res.body && res.body.error == 0) {
        console.log(res.body.data.url);
    }
})

11. 报表服务

在MIS开发中我们会涉及到导出功能,为了满足这一需求,我们抽象了报表服务,快速的进行报表的开发,其使用涉及到create/writeHeaders/writeData/close四个API

var report = new MAS.Report();
report.create({
    type: MAS.Report.TYPE.EXCEL, // 支持excel: MAS.Report.TYPE.EXCEL,csv:MAS.Report.TYPE.CSV 
    mailOpt: { // 是否通过邮件发送生成的报表,可选 
        from: '395693101@qq.com',
        to: '395693101@qq.com',
        subject: 'report test!',
        html: 'xlsx report test!'
    },
    callbackOpt: { // 是否进行回调,获得生成的报表 
        url: 'http://115.159.63.176/mas/report/receive', // 回调地址 
        ext: JSON.stringify({username: 1})  // 附带参数 
    }
}).then(function () {
    return report.writeHeaders([{ // 头部信息,可选 
        "header": "Id",
        "key": "id",
        "width": 50
    }, {
        "header": "Name",
        "key": "name",
        "width": 50
    }, {
        "header": "D.O.B.",
        "key": "DOB",
        "width": 50
    }]);
}).then(function () {
    return report.writeData([ // 写入报表数据 
        {
            "id": 1,
            "name": "John Doe",
            "DOB": "2016-01-01 12:00"
        }, {
            "id": 2,
            "name": "Jane Doe",
            "DOB": "2016-03-03 13:00"
        }
    ]);
}).then(function(){
    return report.writeData([
        {
            "id": 1,
            "name": "John Doe",
            "DOB": "2016-01-01 12:00"
        }, {
            "id": 2,
            "name": "Jane Doe",
            "DOB": "2016-03-03 13:00"
        }
    ]);
}).then(function () {
    return report.close();
}).then(function () {
    done();
})