npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mas-sdk

v0.2.14

Published

mas javascript sdk.

Downloads

7

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与传统数据库的区别在于:

  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', '[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 中实现的步骤如下:

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