mas-js-sdk
v0.2.13
Published
mas javascript sdk.
Downloads
22
Readme
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与传统数据库的区别在于:
- Schema Free/Not free 的差异;
- 数据接口上,MAS 是面向对象的(数据操作接口都是基于 Object 的),开放的(所有移动端都可以直接访问),DB 是面向结构的,封闭的(一般在 Server 内部访问)
目前,MAS针对MIS开发的特点集成了常用的功能:
接下来我们会一一介绍各个功能的使用
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的安全验证有一套严格的流程,通过这个流程我们可以将数据权限细化到一个数据的读写,其流程步骤为:
- 对app的访问权限验证(通过应用创建的appId及appKey进行验证)
- 对用户进行权限验证 (通过MAS.User对象login后获得的uid和token进行验证)
- 对CRUD的Class进行权限验证(判断用户是否在Class的CRUD相关权限列表中)
- 对数据的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 比较查询
- 等于:equalTo
- 不等于: notEqualTo
- 大于:greaterThan
- 大于等于:greaterThanOrEqualTo
- 小于:lessThan
- 小于等于: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', '[email protected]');
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 中实现的步骤如下:
- 写一篇帖子
- 设置帖子的「读」权限为所有人可读。
- 设置帖子的「写」权限为作者可写。
- 保存帖子
实例代码如下:
// 新建一个帖子对象
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)来统一管理论坛的帖子,他可以修改帖子的内容,删除不合适的帖子。
论坛升级之后,用户发布帖子的步骤需要针对上一小节做如下调整:
- 写一篇帖子
- 设置帖子的「读」权限为所有人。
- 设置帖子的「写」权限为作者以及管理员
- 保存帖子
我们可以设想一下,每当论坛产生一篇帖子,就得为管理员添加这篇帖子的「写」权限。
假如做权限管理功能的时候都依赖基于用户的权限管理,那么一旦产生变化就会发现这种实现方式的局限性。
比如新增了一个管理员,新的管理员需要针对目前论坛所有的帖子拥有管理员应有的权限,那么我们需要把数据库现有的所有帖子循环一遍,为新的管理员增加「写」权限。
假如论坛又一次升级了,付费会员享有特殊帖子的读权限,那么我们需要在发布新帖子的时候,设置「读」权限给部分人(付费会员)。这需要查询所有付费会员并一一设置。
毫无疑问,这种实现方式是完全失控的,基于用户的权限管理,在针对简单的私密分享类的应用是可行的,但是一旦产生需求变更,这种实现方式是不被推荐的。
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(
"[email protected]", // from
"[email protected]", // to
"test1", // subject
"hello world!" // html
).then(function (res) {
if (res.body && res.body.error == 0) {
done();
}
});
// 群发邮件
MAS.Mail(
"[email protected]",
["[email protected]", "[email protected]"],
"test1",
"hello world!"
).then(function (res) {
if (res.body && res.body.error == 0) {
done();
}
});
// 携带文件
MAS.Mail(
"[email protected]",
"[email protected]",
"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: '[email protected]',
to: '[email protected]',
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();
})