// 为 AV.Object 创建子类
const Todo = AV.Object.extend('Todo');
// 为该类创建一个新实例
const todo = new Todo();
// 你还可以直接使用 AV.Object 的构造器
const todo = new AV.Object('Todo');
在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将 LeanCloud 里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。
如果你的应用时不时出现 Maximum call stack size exceeded 异常,可能是因为在循环或回调中调用了 AV.Object.extend。有两种方法可以避免这种异常:一种是在循环或回调外声明 class,确保不会对一个 class 执行多次 AV.Object.extend;另一种是将 SDK 更换到 1.4 或更高版本。
注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。
因此,需要原子增减的字段建议使用整数以避免误差,例如 3.14 可以存储为 314,然后在客户端进行相应的转换。
否则,以比较大小为条件查询对象的时候,需要特殊处理,
< a 需改查 < a + e,> a 需改查 > a - e,== a 需改查 > a - e 且 < a + e,其中 e 为误差范围,据所需精度取值,比如 0.0001。
想要建立多对多关系,最简单的办法就是使用 数组。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 中间表 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。
比如说,你有一个用于存储国家和语言对应关系的 Country class,还有一个用于存储学生国籍的 Student class:
name
language
US
English
UK
English
China
Chinese
fullName
nationality
John Doe
US
Tom Sawyer
UK
Ming Li
China
下面的代码可以找到所有来自英语国家的学生:
const studentQuery = new AV.Query('Student');
const countryQuery = new AV.Query('Country');
// 获取所有的英语国家
countryQuery.equalTo('language', 'English');
// 把 Student 的 nationality 和 Country 的 name 关联起来
studentQuery.matchesKeyInQuery('nationality', 'name', countryQuery);
studentQuery.find().then((students) => {
// students 包含 John Doe 和 Tom Sawyer
});
可以通过 select 指定需要返回的属性。下面的代码只获取每个对象的 title 和 content(包括内置属性 objectId、createdAt 和 updatedAt):
const point = new AV.GeoPoint(39.9, 116.4);
// 其他构建 AV.GeoPoint 的方式
const point = new AV.GeoPoint([39.9, 116.4]);
const point = new AV.GeoPoint({ latitude: 39.9, longitude: 116.4 });
现在可以将这个地理位置存储为一个对象的属性:
todo.set('location', point);
地理位置查询
给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 AV.Query 添加 near 条件。下面的代码查找 location 属性值离某一点最近的 Todo 对象:
const query = new AV.Query('Todo');
const point = new AV.GeoPoint(39.9, 116.4);
query.near('location', point);
// 限制为 10 条结果
query.limit(10);
query.find().then((todos) => {
// todos 是包含满足条件的 Todo 对象的数组
});
输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 { "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." },开发者可在客户端进行必要提示。
锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。
数据存储开发指南 · JavaScript
数据存储(LeanStorage)是 LeanCloud 提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端:
我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。
SDK 安装与初始化
请阅读 JavaScript 安装指南。
Web 安全
如果在前端使用 JavaScript SDK,当你准备正式发布的时候,请务必在 控制台 > 设置 > 安全中心 中配置 Web 安全域名,这样可以防止其他人通过外网其他地址盗用你的服务器资源。
不过这样做还不够,因为你的应用是暴露在公网上的,任何人都有可能伪造请求来绕过前面的防范措施。所以 LeanCloud 还提供了其他方法来保护你的应用安全。参见:
对象
AV.Object
AV.Object
是 LeanCloud 对复杂对象的封装,每个AV.Object
包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个AV.Object
上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。比如说,一个保存着单个 Todo 的
AV.Object
可能包含如下数据:数据类型
AV.Object
支持的数据类型包括String
、Number
、Boolean
、Object
、Array
、Date
等等。你可以通过嵌套的方式在Object
或Array
里面存储更加结构化的数据。AV.Object
还支持两种特殊的数据类型Pointer
和File
,可以分别用来存储指向其他AV.Object
的指针以及二进制数据。AV.Object
同时支持GeoPoint
,可以用来存储地理位置信息。参见 GeoPoint。以下是一些示例:
我们不推荐在
AV.Object
里面存储图片、文档等大型二进制数据。每个AV.Object
的大小不应超过 128 KB。如需存储大型文件,可创建AV.File
实例并将将其关联到AV.Object
的某个属性上。参见 文件。注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。
控制台 > 存储 > 数据 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。
若想了解 LeanCloud 是如何保护应用数据的,请阅读 数据和安全。
构建对象
下面的代码构建了一个 class 为
Todo
的AV.Object
:在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将 LeanCloud 里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。
如果你的应用时不时出现
Maximum call stack size exceeded
异常,可能是因为在循环或回调中调用了AV.Object.extend
。有两种方法可以避免这种异常:一种是在循环或回调外声明 class,确保不会对一个 class 执行多次AV.Object.extend
;另一种是将 SDK 更换到 1.4 或更高版本。如果你使用的是 ES6,还可以通过
extends
关键字来创建AV.Object
的子类,然而 SDK 无法自动识别你创建的子类。你需要通过这种方式手动注册一下:这样你就能在
AV.Object
的子类中添加额外的方法和属性了。保存对象
下面的代码将一个 class 为
Todo
的对象存入云端:为了确认对象已经保存成功,我们可以到 控制台 > 存储 > 数据 >
Todo
里面看一下,应该会有一行新的数据产生。点一下这个数据的objectId
,应该能看到类似这样的内容:无需在 控制台 > 存储 > 数据 里面创建新的
Todo
class 即可运行前面的代码。如果 class 不存在,它将自动创建。以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定:
objectId
String
ACL
AV.ACL
createdAt
Date
updatedAt
Date
这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的
AV.Object
不存在这些属性。属性名(keys)只能包含字母、数字和下划线。自定义属性不得以双下划线(
__
)开头或与任何系统保留字段和内置属性(ACL
、className
、createdAt
、objectId
和updatedAt
)重名,无论大小写。属性值(values)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 数据类型。
我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如
CustomData
。属性,采用小驼峰法,如imageUrl
。获取对象
对于已经保存到云端的
AV.Object
,可以通过它的objectId
将其取回:对象拿到之后,可以通过
get
方法来获取各个属性的值。注意objectId
、updatedAt
和createdAt
这三个内置属性不能通过get
获取或通过set
修改,只能由云端自动进行填充。尚未保存的AV.Object
不存在这些属性。如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回
undefined
。如果需要一次性获取返回对象的所有属性(比如进行数据绑定)而非显式地调用
get
,可以利用AV.Object
实例的toJSON
方法:同步对象
当云端数据发生更改时,你可以调用
fetch
方法来刷新对象,使之与云端数据同步:刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,
fetch
操作会丢弃这些修改。为避免这种情况,你可以在刷新时指定 需要刷新的属性,这样只有指定的属性会被刷新(包括内置属性objectId
、createdAt
和updatedAt
),其他属性不受影响。更新对象
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用
save
方法。例如:LeanCloud 会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。
如果想要查看哪些属性有尚未保存的修改,可以调用
dirtyKeys
方法:如果希望撤销尚未保存的修改,可以调用
revert
方法。 直接调用revert()
将撤销所有尚未保存的修改, 还可以额外传入keys
数组作为参数,指定需要撤销的属性,例如:有条件更新对象
通过传入
query
选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回305
错误。例如,用户的账户表
Account
有一个余额字段balance
,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求:query
选项只对已存在的对象有效,不适用于尚未存入云端的对象。query
选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过AV.Query
查询AV.Object
再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。更新计数器
设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 原子操作 来增加或减少一个属性内保存的数字:
可以指定需要增加或减少的值。若未指定,则默认使用
1
。注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 因此,需要原子增减的字段建议使用整数以避免误差,例如
3.14
可以存储为314
,然后在客户端进行相应的转换。 否则,以比较大小为条件查询对象的时候,需要特殊处理,< a
需改查< a + e
,> a
需改查> a - e
,== a
需改查> a - e
且< a + e
,其中e
为误差范围,据所需精度取值,比如0.0001
。更新数组
更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据:
AV.Object.add('arrayKey', value)
将指定对象附加到数组末尾。
AV.Object.addUnique('arrayKey', value)
如果数组中不包含指定对象,则将该对象加入数组。对象的插入位置是随机的。
AV.Object.remove('arrayKey', value)
从数组字段中删除指定对象的所有实例。
例如,
Todo
用一个alarms
属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性:删除对象
下面的代码从云端删除一个
Todo
对象;如果只需删除对象的一个属性,可以用
unset
:删除对象是一个较为敏感的操作,我们建议你阅读 ACL 权限管理开发指南 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。
批量操作
可以在一次请求中包含多个构建、保存、删除和同步对象的操作:
下面的代码将所有
Todo
的isComplete
设为true
:虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。
数据模型
对象之间可以产生关联。拿一个博客应用来说,一个
Post
对象可以与许多个Comment
对象产生关联。LeanCloud 支持三种关系:一对一、一对多、多对多。一对一、一对多关系
一对一、一对多关系可以通过将
AV.Object
保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个Comment
指向一个Post
。下面的代码会创建一个含有单个
Comment
的Post
:云端存储时,会将被指向的对象用
Pointer
的形式存起来。你也可以用objectId
来指向一个对象:请参阅 关系查询 来了解如何获取关联的对象。
多对多关系
想要建立多对多关系,最简单的办法就是使用 数组。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 中间表 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。
我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。
我们知道设计数据模型并不容易,所以我们专门写了一篇 数据模型设计指南 来详细介绍这部分内容,推荐你去看一下。
序列化和反序列化
在实际的开发中,把
AV.Object
当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此AV.Object
也提供了序列化和反序列化的方法。序列化:
AV.Object
还提供了另一个方法toJSON()
。它们的区别是toJSON()
得到的对象仅包含对象的 payload,一般用于展示,而toFullJSON()
得到的对象包含了元数据,一般用于传输。在使用时请注意区分。反序列化:
查询
我们已经了解到如何从云端获取单个
AV.Object
,但你可能还会有一次性获取多个符合特定条件的AV.Object
的需求,这时候就需要用到AV.Query
了。基础查询
执行一次基础查询通常包括这些步骤:
AV.Query
;下面的代码获取所有
lastName
为Smith
的Student
:查询条件
可以给
AV.Object
添加不同的条件来改变获取到的结果。下面的代码查询所有
firstName
不为Jack
的对象:对于能够排序的属性(比如数字、字符串),可以进行比较查询:
可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是
AND
的关系:可以通过指定
limit
限制返回结果的数量(默认为100
):由于性能原因,
limit
最大只能设为1000
。即使将其设为大于1000
的数,云端也只会返回 1,000 条结果。如果只需要一条结果,可以直接用
first
:可以通过设置
skip
来跳过一定数量的结果:把
skip
和limit
结合起来,就能实现翻页功能:需要注意的是,
skip
的值越高,查询所需的时间就越长。作为替代方案,可以通过设置createdAt
或updatedAt
的范围来实现更高效的翻页,因为它们都自带索引。对于能够排序的属性,可以指定结果的排序规则:
还可以为同一个查询添加多个排序规则;
下面的代码可用于查找包含或不包含某一属性的对象:
可以用
matchesKeyInQuery
查找某一属性值为另一查询返回结果的对象。比如说,你有一个用于存储国家和语言对应关系的
Country
class,还有一个用于存储学生国籍的Student
class:下面的代码可以找到所有来自英语国家的学生:
可以通过
select
指定需要返回的属性。下面的代码只获取每个对象的title
和content
(包括内置属性objectId
、createdAt
和updatedAt
):select
支持点号(author.firstName
),详见 点号使用指南。 另外,字段名前添加减号前缀表示反向选择,例如-author
表示不返回author
字段。 反向选择同样适用于内置字段,比如-objectId
,也可以和点号组合使用,比如-pubUser.createdAt
。对于未获取的属性,可以通过对结果中的对象进行
fetch
操作来获取。参见 同步对象。字符串查询
可以用
startsWith
来查找某一属性值以特定字符串开头的对象。和 SQL 中的LIKE
一样,你可以利用索引带来的优势:可以用
contains
来查找某一属性值包含特定字符串的对象:和
startsWith
不同,contains
无法利用索引,因此不建议用于大型数据集。注意
startsWith
和contains
都是 区分大小写 的,所以上述查询会忽略Lunch
、LUNCH
等字符串。如果想查找某一属性值不包含特定字符串的对象,可以使用
matches
进行基于正则表达式的查询:不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class,因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以了解一下 全文搜索 功能。
使用查询时如果遇到性能问题,可参阅 查询性能优化。
数组查询
下面的代码查找所有数组属性
tags
包含工作
的对象:下面的代码查询数组属性长度为 3 (正好包含 3 个标签)的对象:
下面的代码查找所有数组属性
tags
同时包含工作
、销售
和会议
的对象:如需获取某一属性值包含一列值中任意一个值的对象,可以直接用
containedIn
而无需执行多次查询。下面的代码构建的查询会查找所有priority
为1
或2
的 todo 对象:反过来,还可以用
notContainedIn
来获取某一属性值不包含一列值中任何一个的对象。关系查询
查询关联数据有很多种方式,常见的一种是查询某一属性值为特定
AV.Object
的对象,这时可以像其他查询一样直接用equalTo
。比如说,如果每一条博客评论Comment
都有一个post
属性用来存放原文Post
,则可以用下面的方法获取所有与某一Post
相关联的评论:如需获取某一属性值为另一查询结果中任一
AV.Object
的对象,可以用matchesQuery
。下面的代码构建的查询可以找到所有包含图片的博客文章的评论:如需获取某一属性值不是另一查询结果中任一
AV.Object
的对象,则使用doesNotMatchQuery
。有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用
include
。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章:可以用 dot 符号(
.
)来获取多级关系,例如post.author
,详见 点号使用指南。可以在同一查询上应用多次
include
以包含多个属性。通过这种方法获取到的对象同样接受first
、get
等AV.Query
辅助方法。通过
include
进行多级查询的方式不适用于数组属性内部的AV.Object
,只能包含到数组本身。关系查询的注意事项
LeanCloud 云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,
limit
默认为100
,最大1000
),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出limit
,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有limit
数量以内的结果会被填入主查询。我们建议采用以下方案进行改进:
limit
设为1000
。skip
值来遍历所有记录(注意skip
的值较大时可能会引发性能问题,因此不是很推荐)。统计总数量
如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用
count
来代替find
。比如说,查询有多少个已完成的 todo:组合查询
组合查询就是把诸多查询条件用一定逻辑合并到一起(
OR
或AND
)再交给云端去查询。组合查询不支持在子查询中包含
GeoPoint
或其他非过滤性的限制(例如near
、withinGeoBox
、limit
、skip
、ascending
、descending
、include
)。OR 查询
OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于
3
或者已经完成了的 todo:使用 OR 查询时,子查询中不能包含
GeoPoint
相关的查询。AND 查询
使用 AND 查询的效果等同于往
AV.Query
添加多个条件。下面的代码构建的查询会查找创建时间在2016-11-13
和2016-12-02
之间的 todo:单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询:
查询性能优化
影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。
count
(需要扫描所有数据)skip
跳过较多的行数(相当于需要先查出被跳过的那些行)LiveQuery
LiveQuery 衍生于
AV.Query
,并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用
AV.Query
并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的
AV.Query
。订阅成功后,一旦有符合AV.Query
的AV.Object
发生变化,云端就会主动、实时地将信息通知到客户端。LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。
启用 LiveQuery
进入 控制台 > 存储 > 设置,在 其他 里面勾选 启用 LiveQuery,然后将下面的 npm 模块添加到项目中即可:
或者使用
script
标签:可以在 SDK 安装与初始化 中找到完整设置方法。
Demo
下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果:
使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下:
注意按以上顺序操作。在网页应用中使用 Signup 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。
LiveQuery 公开课 涵盖了许多开发者关心的问题和解答。
构建订阅
首先创建一个普通的
AV.Query
对象,添加查询条件(如有),然后进行订阅操作:LiveQuery 不支持内嵌查询,也不支持返回指定属性。
订阅成功后,就可以接收到和
AV.Object
相关的更新了。假如在另一个客户端上创建了一个Todo
对象,对象的title
设为更新作品集
,那么下面的代码可以获取到这个新的Todo
:此时如果有人把
Todo
的content
改为把我最近画的插画放上去
,那么下面的代码可以获取到本次更新:事件处理
订阅成功后,可以选择监听如下几种数据变化:
create
update
enter
leave
delete
create
事件当有新的满足
AV.Query
查询条件的AV.Object
被创建时,create
事件会被触发。下面的object
就是新建的AV.Object
:update
事件当有满足
AV.Query
查询条件的AV.Object
被更新时,update
事件会被触发。下面的object
就是有更新的AV.Object
:enter
事件当一个已存在的、原本不符合
AV.Query
查询条件的AV.Object
发生更新,且更新后符合查询条件,enter
事件会被触发。下面的object
就是进入AV.Query
的AV.Object
,其内容为该对象最新的值:注意区分
create
和enter
的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么enter
事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么create
事件会被触发。leave
事件当一个已存在的、原本符合
AV.Query
查询条件的AV.Object
发生更新,且更新后不符合查询条件,leave
事件会被触发。下面的object
就是离开AV.Query
的AV.Object
,其内容为该对象最新的值:delete
事件当一个已存在的、原本符合
AV.Query
查询条件的AV.Object
被删除,delete
事件会被触发。下面的object
就是被删除的AV.Object
的objectId
:取消订阅
如果不再需要接收有关
AV.Query
的更新,可以取消订阅。断开连接
断开连接有几种情况:
如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。
而另外一种极端情况——当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。
LiveQuery 的注意事项
因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且 LeanCloud 已经提供了 即时通讯 的服务。LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。
文件
有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用
AV.Object
保存,此时文件对象AV.File
便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。构建文件
可以通过 base64 编码的字符串构建文件:
还可以通过字节数组构建文件:
此外,浏览器环境下还可以通过 Blob 构建文件,Node.js 环境下还可以通过 Buffer、Stream 构建文件。
React Native 环境下,可以通过本地路径构建文件:
除此之外,还可以通过 URL 构建文件:
通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。
LeanCloud 会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定
Content-Type
(一般称为 MIME 类型):与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。如果开发的是 web 应用,可以先在页面上放一个按钮:
然后在一个点击事件处理函数中获取这个文件:
这里上传的文件名字叫做
avatar.jpg
。需要注意:objectId
,所以在一个应用内是允许多个文件重名的。AV.File
保存一个 PNG 格式的图像,那么扩展名应为.png
。application/octet-stream
。保存文件
将文件保存到云端后,便可获得一个永久指向该文件的 URL:
文件上传后,可以在
_File
class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的objectId
和 URL。如果希望在文件的 url 中保留文件名,可以在保存时传入
keepFileName
参数。已经保存到云端的文件可以关联到
AV.Object
:也可以通过构建
AV.Query
进行查询:需要注意的是,内部文件(上传到 LeanCloud 文件服务的文件)的
url
字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 因此,通过 url 字段查询文件仅适用于外部文件(直接保存外部 URL 到_File
表创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。注意,如果文件被保存到了
AV.Object
的一个数组属性中,那么在查询AV.Object
时如果需要包含文件,则要用到AV.Query
的include
方法。比如说,在获取所有标题为买蛋糕
的 todo 的同时获取附件中的文件:上传进度监听
上传过程中可以实时向用户展示进度:
文件元数据
上传文件时,可以用
metaData
添加额外的属性。文件一旦保存,metaData
便不可再修改。删除文件
下面的代码从云端删除一个文件:
默认情况下,文件的删除权限是关闭的,需要进入 控制台 > 存储 > 数据 >
_File
,选择 其他 > 权限设置 >delete
来开启。启用 HTTPS 域名
如果希望使用 HTTPS 域名来访问文件,需要进入 控制台 > 存储 > 设置 > 自定义文件域名,勾选 启用 HTTPS 域名。HTTPS 文件流量无免费的使用额度,收费标准将在该选项开启时显示。
「启用 HTTPS 域名」会影响到 API 返回的文件地址是 HTTPS 还是 HTTP 类型的 URL。需要注意的是,即使没有启用这一选项,终端仍然可以选择使用 HTTPS URL 来访问文件,但由此会产生 HTTPS 流量扣费。
在启用文件 HTTPS 域名之后,之前已保存在
_File
表中的文件的 URL 会自动被转换为以 HTTPS 开头。如果取消 HTTPS 域名,已经改为 HTTPS 域名的文件不会变回到 HTTP。LeanCloud 即时通讯组件也使用
AV.File
来保存消息的图片、音频等文件,并且把文件的地址写入到了消息内容中。当文件 HTTPS 域名被开启后,之前历史消息中的文件地址不会像_File
表那样被自动转换,而依然保持 HTTP。CDN 加速
中国节点的文件存储服务自带 CDN 加速访问,但不包括海外 CDN 加速。商用版应用可发工单申请开启海外 CDN 加速(开启后海外访问文件 http/https 流量费用分别上调为 0.40 元/GB、0.60 元/GB。 国际版没有现成的 CDN 加速访问服务,需要用户自行配置(步骤)。
以 CloudFront 加速服务为例,配置过程如下:
Promise
每一个在 LeanCloud JavaScript SDK 中的异步方法都会返回一个
Promise
,可以用于处理该异步方法的完成与异常。下面的示例代码在查询到一个AV.Object
后对其进行更新:then
方法每一个
Promise
都有一个叫then
的方法,这个方法接受一对 callback。第一个 callback 在Promise
被解决(resolved
,也就是正常运行)的时候调用,第二个会在Promise
被拒绝(rejected
,也就是遇到错误)的时候调用:其中第二个参数是可选的。
你还可以使用
catch
方法,将逻辑写成:将
Promise
组织在一起Promise 比较神奇,可以代替多层嵌套方式来解决发送异步请求代码的调用顺序问题。如果一个
Promise
的回调会返回一个Promise
,那么第二个then
里的 callback 在第一个then
的 callback 没有解决前是不会解决的,也就是所谓 Promise Chain。错误处理
如果任意一个在链中的
Promise
抛出一个异常的话,所有接下来可能成功的 callback 都会被跳过直到遇到一个处理错误的 callback。通常来说,在正常情况的回调函数链的末尾,加一个错误处理的回调函数,是一种很常见的做法。
利用
catch
方法可以将上述代码改写为:async
和await
async
和await
能让你以更接近同步代码的方式使用 Promise:如果你想更深入地了解和学习 Promise,包括如何对并行的异步操作进行控制,我们推荐阅读 JavaScript Promise迷你书(中文版)这本书。
GeoPoint
LeanCloud 允许你通过将
AV.GeoPoint
关联到AV.Object
的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个
AV.GeoPoint
并将其纬度(latitude
)设为39.9
,经度(longitude
)设为116.4
:现在可以将这个地理位置存储为一个对象的属性:
地理位置查询
给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的
AV.Query
添加near
条件。下面的代码查找location
属性值离某一点最近的Todo
对象:像
ascending
和descending
这样额外的排序条件会获得比默认的距离排序更高的优先级。若要限制结果和给定地点之间的距离,可以参考 API 文档中的
withinKilometers
、withinMiles
和withinRadians
参数。若要查询在某一矩形范围内的对象,可以用
withinGeoBox
:GeoPoint 的注意事项
GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 另外,每个对象最多只能有一个类型为 GeoPoint 的属性。
用户
用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个
AV.User
类来方便应用使用各项用户管理的功能。AV.User
是AV.Object
的子类,这意味着任何AV.Object
提供的方法也适用于AV.User
,唯一的区别就是AV.User
提供一些额外的用户管理相关的功能。每个应用都有一个专门的_User
class 用于存放所有的AV.User
。用户的属性
AV.User
相比一个普通的AV.Object
多出了以下属性:username
:用户的用户名。password
:用户的密码。email
:用户的电子邮箱。emailVerified
:用户的电子邮箱是否已验证。mobilePhoneNumber
:用户的手机号。mobilePhoneVerified
用户的手机号是否已验证。在接下来对用户功能的介绍中我们会逐一了解到这些属性。
注册
用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程:
新建
AV.User
的操作应使用signUp
而不是save
,但以后的更新操作就可以用save
了。如果收到
202
错误码,意味着_User
表里已经存在使用同一username
的账号,此时应提示用户换一个用户名。除此之外,每个用户的email
和mobilePhoneNumber
也需要保持唯一性,否则会收到203
或214
错误。可以考虑在注册时把用户的username
设为与email
相同,这样用户可以直接 用邮箱重置密码。采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 在客户端,应用切勿再次对密码加密,这会导致 重置密码 等功能失效。
登录
下面的代码用用户名和密码登录一个账户:
邮箱登录
下面的代码用邮箱和密码登录一个账户:
单设备登录
某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现:
账户锁定
输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码
{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }
,开发者可在客户端进行必要提示。锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。
验证邮箱
可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,
emailVerified
会被设为false
。在应用的 控制台 > 存储 > 设置 中,可以开启 从客户端注册邮箱或者更新邮箱时,发送验证邮件 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件:
用户点击邮件内的链接后,
emailVerified
会变为true
。如果用户的email
属性为空,则该属性永远不会为true
。验证手机号
和 验证邮箱 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,
mobilePhoneVerified
会被设为false
。 在应用的「控制台 > 存储 > 用户 > 设置」中,可以开启阻止未验证手机号的用户登录的选项。可以用下面的代码发送一条新的验证码:(如果相应用户的
mobilePhoneVerified
已经为true
,那么验证短信不会发送)用户填写验证码后,调用下面的方法来完成验证。
mobilePhoneVerified
将变为true
:绑定、修改手机号之前先验证
除了在用户绑定、修改手机号之后进行验证,LeanCloud 也支持在用户绑定或修改手机号之前先通过短信验证。 也就是说,绑定手机号或修改手机号时先请求发送验证码(用户需处于登录状态),再凭短信验证码完成绑定或修改操作。
当前用户
用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户:
会话信息会长期有效,直到用户主动登出:
设置当前用户
用户登录后,云端会返回一个 session token 给客户端,它会由 SDK 缓存起来并用于日后同一
AV.User
的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个AV.User
发起的请求了。以下是一些应用可能需要用到 session token 的场景:
AV.User.current().getSessionToken()
获取到当前用户的 session token)。下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效):
请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。
如果在 控制台 > 存储 > 设置 中勾选了 密码修改后,强制客户端重新登录,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到
403 (Forbidden)
错误。下面的代码检查 session token 是否有效:
重置密码
我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。
邮箱重置密码的流程如下:
首先让用户填写注册账户时使用的邮箱,然后调用下面的方法:
上面的代码会查询
_User
表中是否有对象的email
属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让username
与email
保持一致,也可以单独收集用户的邮箱并将其存为email
。密码重置邮件的内容可在应用的 控制台 > 设置 > 邮件模版 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考 自定义邮件验证和重设密码页面。
用户的查询
可以直接构建一个针对
_User
的AV.Query
来查询用户:为了安全起见,新创建的应用的
_User
表默认关闭了find
权限,这样每位用户登录后只能查询到自己在_User
表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的find
查询权限。除此之外,还可以在 云引擎 里封装用户查询相关的方法,这样就无需开放_User
表的find
权限。可以参见 用户对象的安全 来了解
_User
表的一些限制,还可以阅读 数据和安全 来了解更多 class 级权限设置的方法。关联用户对象
关联
AV.User
的方法和AV.Object
是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书:用户对象的安全
AV.User
类自带安全保障,只有通过logIn
或者signUp
这种经过鉴权的方法获取到的AV.User
才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。这样设计是因为
AV.User
中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。下面的代码展现了这种安全措施:
通过
AV.User.current()
获取的AV.User
总是经过鉴权的。要查看一个
AV.User
是否经过鉴权,可以调用isAuthenticated
方法。通过经过鉴权的方法获取到的AV.User
无需进行该检查。注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 重置密码 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到
null
。其他对象的安全
对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由
AV.ACL
对象组成的访问控制表。请参阅 ACL 权限管理开发指南。第三方账户登录
LeanCloud 支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。
例如以下的代码展示了终端用户使用微信登录的处理流程:
AV.User#loginWithAuthData
系列方法需要两个参数来唯一确定一个账户:weixin
,该名字由应用层自己决定。thirdPartyData
(一般包括uid
、token
、expires
等信息,与具体的第三方平台有关)。LeanCloud 云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回
200 OK
状态码,同时附上用户的信息(包括sessionToken
)。如果第三方平台的信息没有和任何账户关联,客户端会收到201 Created
状态码,意味着新账户被创建,同时附上用户的objectId
、createdAt
、sessionToken
和一个自动生成的username
,例如:这时候我们会看到
_User
表中出现了一条新的账户记录,账户中有一个名为authData
的列,保存了第三方平台的授权信息。出于安全考虑,authData
不会被返回给客户端,除非它属于当前用户。开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到 LeanCloud 云端来登录。
鉴权数据的保存
_User
class 中的authData
是一个以平台名为键名,鉴权信息为键值的 JSON 对象。一个关联了微信账户的用户应该会有下列对象作为
authData
:而一个关联了微博账户的用户,则会有如下的
authData
:我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其
authData
可能会是这样的:理解
authData
的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候,LeanCloud 云端首先会查找账户系统(_User 表),看看是否存在 authData.weixin.openid = “OPENID” 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的
authData
属性中,并将新账户的数据当成结果返回。为避免出现重复数据,
_User
class 中每个用户的authData.<PLATFORM>.<uid>
需要保持独一无二,这可以通过创建唯一索引来完成(通常 LeanCloud 云端会自动创建这一索引,如果不存在这一索引,开发者可联系我们人工追加此索引)。自动验证第三方平台授权信息
为了确保账户数据的有效性,LeanCloud 云端还支持对部分平台的 access token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回
invalid authData
错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 access token 的有效性(可通过云引擎的 onLogin hook 函数来实现)。如果希望使用这一功能,则在开始使用前,需要在 控制台 > 组件 > 社交 配置相应平台的 应用 ID 和 应用 Secret Key。
如果不希望云端自动验证 access token,可以在 控制台 > 存储 > 设置 里面取消勾选 第三方登录时,验证用户 AccessToken 合法性。
配置平台账号的目的在于创建
AV.User
时,LeanCloud 云端会使用相关信息去校验请求参数thirdPartyData
的合法性,确保AV.User
实际对应着一个合法真实的用户,确保平台安全性。解除与第三方账户的关联
如果将 _User 表
authData
中对应平台的 json 数据设为null
,则意味着解除与该平台的关联,SDK 层提供了AV.User#dissociateAuthData(platform)
系列方法来完成这一操作。例如,下面的代码可以解除用户和微信账户的关联:
扩展:第三方登录时补充完整的用户信息
有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。
这时要使用
loginWithauthData
登录接口的failOnNotExist
参数并将其设置为true
。服务端会判断是否已存在能匹配上的authData
,如果不存在则会返回211
错误码和Could not find user
报错信息。开发者根据这个211
错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个AV.User
对象,保存上述补充数据,再次调用loginWithauthData
接口进行登录,并 不再传入failOnNotExist
参数。示例代码如下:扩展:接入 UnionID 体系,打通不同子产品的账号系统
随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。
当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。
微信官方为了解决这个问题,引入了
UnionID
的体系,以下为其官方说明:其他平台,如 QQ 和微博,与微信的设计也基本一致。
LeanCloud 支持
UnionID
体系。你只需要给loginWithauthData
和associateWithauthData
接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括:下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。
假设 LeanCloud 在微信开放平台上有两个应用,一个是「LeanCloud 通讯」,一个是「LeanCloud 技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了
wxleanoffice
和wxleansupport
作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(_User 表中的同一条记录),同时我们决定将wxleanoffice
平台作为主账号平台。假设对于用户 A,微信给 ta 为 LeanCloud 分配的 UnionId 为
unionid4a
,而对两个应用的授权信息分别为:现在,用户 A 在「LeanCloud 通讯」中通过微信登录,其调用请求为:
如果用户 A 是第一次在「LeanCloud 通讯」中通过微信登录,那么 _User 表中会增加一个新用户(假设其 objectId 为
ThisIsUserA
),其authData
的结果如下:可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了
asMainAccount
为 true,所以 authData 的第一级子目录中增加了_weixin_unionid
的键值对,这里的weixin
就是我们指定的unionIdPlatform
的值。_weixin_unionid
这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的asMainAccount
的值决定的:asMainAccount
为 true 时,LeanCloud 后台会在authData
下面增加名为_{unionIdPlatform}_unionid
的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。asMainAccount
为 false 时,LeanCloud 后台不会在authData
下面增加名为_{unionIdPlatform}_unionid
的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的authData
属性里,同时返回主账号对应的 _User 表记录;如果通过提供的 UnionID 可以找不到主账号,则会根据平台的openid
去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。接下来,用户 A 继续在「LeanCloud 技术支持」中进行微信登录,其登录逻辑为:
与「LeanCloud 通讯」中的登录过程相比,在「LeanCloud 技术支持」这个应用中,我们在登录时只是将
asMainAccount
设为了 false。 这时我们看到,本次登录得到的还是 objectId 为ThisIsUserA
的 _User 表记录(同一个账户),同时该账户的authData
属性中发生了变化,多了wxleansupport
的数据,如下:在新的登录方式中,当一个用户以「平台名为
wxleanoffice
、uid 为officeopenid
、UnionID 为unionid4a
」的第三方鉴权信息登录得到新的AV.User
后,接下来这个用户以「平台名为wxleansupport
、uid 为supportopenid
、UnionID 为unionid4a
」的第三方鉴权信息登录时,LeanCloud 判定是同样的 UnionID,就直接把来自wxleansupport
的新用户数据加入到已有账户的authData
里了,不会再创建新的账户。这样一来,LeanCloud 后台通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个
AV.User
上,实现互通。该如何指定 unionIdPlatform
从上面的例子可以看出,使用 UnionID 登录的时候,需要指定
unionIdPlatform
的主要目的,就是为了便于查找已经存在的唯一主账号。LeanCloud 云端会在主账号对应账户的authData
属性中增加一个_{unionIdPlatform}_unionid
键值对来标记唯一性,终端用户在其他应用中登录的时候,LeanCloud 云端会根据参数中提供的uniondId
+unionIdPlatform
的组合,在_User
表中进行查找,这样来确定唯一的既存主账号。本来
unionIdPlatform
的取值,应该是开发者可以自行决定的,但是 Javascript SDK 基于易用性的目的,在loginWithAuthDataAndUnionId
之外,还额外提供了两个接口:AV.User.loginWithQQAppWithUnionId
,这里默认使用qq
作为unionIdPlatform
。AV.User.loginWithWeappWithUnionId
,这里默认使用weixin
作为unionIdPlatform
。从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 Javascript SDK 的默认值来设置
unionIdPlatform
,即:weixin
作为unionIdPlatform
;qq
作为unionIdPlatform
;weibo
作为unionIdPlatform
;unionIdPlatform
的名字,只要自己保证多个应用间统一即可。主副应用不同登录顺序出现的不同结果
上面的流程是用户先登录了「LeanCloud 通讯」这个主应用,然后再登录「LeanCloud 技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢?
用户 B 首先登录副应用的时候,提供了「平台名为
wxleansupport
、uid 为supportopenid
、UnionID 为unionid4a
」的第三方鉴权信息,并且指定「UnionIDPlatform 为weixin
、asMainAccount
为 false」(与上面的调用完全一致),此时 LeanCloud 后台由于找不到存在的 UnionID,会新建一个AV.User
对象,该账户authData
结果为:用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为
wxleanoffice
、uid 为officeopenid
、UnionID 为unionid4a
」的第三方鉴权信息,以及「UnionIDPlatform 为weixin
、asMainAccount
为 true」的参数进行登录,此时 LeanCloud 后台由于找不到存在的 UnionID,会再次新建一个AV.User
对象,该账户authData
结果为:还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。
存量账户如何通过 UnionID 实现关联
还是以我们的两个子产品「LeanCloud 通讯」(后续以「产品 1」指代)和「LeanCloud 技术支持为例」(后续以「产品 2」指代),在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户:
此时的存量账户表如下所示:
现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 objectId 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「LeanCloud 通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用
asMainAccount=true
参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时_User
表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 objectId 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是:
以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下:
之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下:
如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为:
匿名用户
将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户:
可以像给普通用户设置属性那样给匿名用户设置
username
、password
、email
等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够:下面的代码为一名匿名用户设置用户名和密码:
下面的代码检查当前用户是否为匿名用户:
如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。
角色
随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,LeanCloud 支持基于角色的权限管理。请参阅 ACL 权限管理开发指南。
全文搜索
全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读 全文搜索指南。
Push 通知
通过 JavaScript SDK 也可以向移动设备推送消息。
一个简单例子推送给所有订阅了
public
频道的设备:这就向订阅了
public
频道的设备发送了一条内容为public message
的消息。如果希望按照某个
_Installation
表的查询条件来推送,例如推送给某个installationId
的 Android 设备,可以传入一个AV.Query
对象作为where
条件:AV.Push
的更多使用信息参考 API 文档 AV.Push。更多推送的查询条件和格式,请查阅 消息推送指南。WebView
JS SDK 支持在各种 WebView 中使用,包括 PhoneGap、Cordova、微信 WebView 等。
Android WebView
如果是 Android WebView,在 Native 代码创建 WebView 的时候你需要打开几个选项,这些选项生成 WebView 的时候默认并不会被打开,需要配置:
因为我们 JS SDK 目前使用了
window.localStorage
,所以你需要开启 WebView 的localStorage
:如果你希望直接调试手机中的 WebView,也同样需要在生成 WebView 的时候设置远程调试,具体使用方式请参考 Google 官方文档。
注意:这种调试方式仅支持 Android 4.4 以上版本(含 4.4)。
如果你是通过 WebView 来开发界面,Native 调用本地特性的 Hybrid 方式开发你的 App。比较推荐的开发方式是:通过 Chrome 的开发者工具开发界面部分,当界面部分完成,与 Native 再来做数据连调,这种时候才需要用 Remote debugger 方式在手机上直接调试 WebView。这样做会大大节省你开发调试的时间,不然如果界面都通过 Remote debugger 方式开发,可能效率较低。
为了防止通过 JavaScript 反射调用 Java 代码访问 Android 文件系统的安全漏洞,在 Android 4.2 以后的系统中,WebView 中间只能访问通过
@JavascriptInterface
标记过的方法。如果你的目标用户覆盖 4.2 以上的机型,请注意加上这个标记,以避免出现Uncaught TypeError
。