开发向导
上次更改时间 2015/12/08
1.安装与设置
创建一个帐号
首先,注册并登录Wilddog账号,进入控制面板。然后,在控制面板中,添加一个新的应用。
你会得到一个应用的URL https://.wilddogio.com/。你可以把这个URL理解为云端数据库的地址。
安装Wilddog
使用Javascript SDK非常简单。你只需在HTML文件中加入一个script标签。
<script src="https://cdn.wilddog.com/js/client/current/wilddog.js"></script>
在Node.js上使用Wilddog node.js版API与javascript版完全一样。Wilddog客户端在Node.js上运行非常简单,首先需要通过npm安装Wilddog模块
$npm install wilddog –save
使用require在你的应用中使用wilddog
var Wilddog = require("wilddog");
Typescript用户typescript 调用原生js需要有一个 .d.ts文件。 在这里可以找到
在 Ionic 项目中使用
Ionic 是一个利用 html5 开发混合手机 APP 的前端 SDK ,由于 Ionic 使用 Angular ,所以开发者在开发 Ionic APP 时可以使用 wild-angular 来简化 wilddog 的一些操作。
现在我们可以使用 Ionic 提供的命令行命令来安装 Ionic:
$ npm install -g ionic
如果在 Mac 电脑开发应用并且希望运行在 ios 设备上,需要先安装 XCode ,然后用 npm 安装 ios-sim:
$ npm install -g ios-sim
现在我们可以使用 Ionic 的命令行工具来创建一个空白的 Ionic 应用模板:
$ ionic start myapp blank
使用下面的命令行可以告诉 Ionic 我们的应用要适配 ios 和 Android :
$ ionic platform add ios
$ ionic platform add android
集成 Wilddog:
在 html 文件中,在引入自己的 app.js文件之前,我们引入 Wilddog 和 wild-angular 作为依赖,
<!-- Wilddog -->
<script src="https://cdn.wilddog.com/sdk/js/current/wilddog.js"></script>
<!-- wild-angular -->
<script src="https://cdn.wilddog.com/libs/wild-angular/0.0.1/wild-angular.min.js"></script>
在自己的 app.js 文件中把 Wilddog 作为依赖注入到我们的 module 中:
angular.module("starter", ["ionic", "wilddog"])
现在我们就可以使用 wild-angular 的 wilddogObject、 wilddogArray、$wilddogAuth 来对数据进行操作了。
提示和建议
1,我们建议你直接使用野狗官方提供的SDK地址。这样,你将无需更新任何代码,即可获得更新。
2,wilddog.js是经过大量测试的RELEASE版本。
3,野狗全站均支持Spdy 3.1和Gzip压缩。我们正在尝试更高的压缩比,例如SDCH,Http2来进一步提升静态资源加载速度。
4,不用担心Https的性能。我们对Https进行了极致的优化。野狗的官网,直到windows.onload事件触发,也只花费了不到500ms。
2. 了解数据
数据是一棵 JSON 树
所有的数据都存储在各个 JSON 对象中,没有任何表的概念。当你把数据添加到这棵json 树中,这些数据就变成这棵树的子树。比如,我们在users/mchen 下增加 widget后,我们的数据是这样的:
{
"users": {
"mchen": {
"friends": { "brinchen": true },
"name": "Mary Chen",
// 新数据节点会增加在已经存在的JSON树中
"widgets": { "one": true, "three": true }
},
"brinchen": { ... },
"hmadi": { ... }
}
}
创建一个Wilddog 对象引用
在html中读写wilddog数据,需要创建一个Wilddog对象引用, 要操作和同步哪些数据取决于创建 Wilddog对象引用时传入的URL
new Wilddog(‘https://.wilddogio.com/web/data’);
创建一个Wilddog引用并不是直接访问这个URL,或创建一个连接。数据直到需要的时候才会传输。一旦这个数据被查询,这个数据会一直与服务端保持一致。
你可以直接访问一个子节点:
new Wilddog('https://<appId>.wilddogio.com/web/data/users/mchen/name');
你还可以通过 child接口进行相对路径访问:
var rootRef = new Wilddog('https://<appId>.wilddogio.com/web/data');
rootRef.child('users/mchen/name');
Wilddog 中的数组
Wilddog并不天然支持数组,当我们想存数组时,我们把数组变成对象:
// 原始数据
['hello', 'world']
// 存储后的新数据
{0: 'hello', 1: 'world'}
需要注意的是,如果某节点的所有子节点的key都是整数,且0到key的最大值之间,超过一半的key都有非空的值,那么wilddog客户端会将它们当作数组来处理。
限制和约束
描述 | 约束 | 备注 |
---|---|---|
树的深度 | 32 | |
key的长度 | 768bytes | UTF-8 编码,不能包含. $ # [ ] /和 ASCII控制字符0-31和127 |
一个叶子节点的数据大小 | 1mb | UTF-8 编码 |
通过SDK写入的数据大小限制 | 2mb | UTF-8 编码 |
通过 REST 写入数据大小限制 | 4mb | |
一次能读取的节点 | 2000 | |
一次条件查询能返回的最大条数 | 500 | 如使用 limitToFirst()、limitToLast()等 |
3.保存数据
保存数据的方式
method description
set() 写入和替换当前路径的数据
update() 修改部分子节点的数据
push() 在当前节点下新增一个数据,数据的key随机生成
transaction() 一个复杂数据被并发更新导致数据错误,使用事务防止数据被并发更新
用 set() 写数据
set 是 Wilddog 最基本的写数据操作。set() 设置当前节点的值,如果当前节点已经存在值,set 会将旧值替换成新值。为了理解set的工作原理,我们创建一个简单博客app,这个博客的app储存在这里:
var ref = new Wilddog("https://<appId>.wilddogio.com/web/saving-data/wildblog");
我们用唯一的用户名来标识一个用户,存储他们的全名和生日。首先,我们创建一个引用,然后用set储存数据,set可以传入string, number ,boolean, object 类型:
var usersRef = ref.child("users");
usersRef.set({
alanisawesome: {
date_of_birth: "June 23, 1912",
full_name: "Alan Turing"
},
gracehop: {
date_of_birth: "December 9, 1906",
full_name: "Grace Hopper"
}
});
这时,数据被嵌套保存到了相应的位置上, 完成上面的过程,你也可以直接这样做:
usersRef.child("alanisawesome").set({
date_of_birth: "June 23, 1912",
full_name: "Alan Turing"
});
usersRef.child("gracehop").set({
date_of_birth: "December 9, 1906",
full_name: "Grace Hopper"
});
这两种方式的区别是,如果 /user 下面原来有数据的时候,第一种方式会把数据全部覆盖掉,而第二种方式只会覆盖 alanisawesome 和gracehop 两个子节点。
更新已经存在的数据
如果你想同时更新多个子节点,而不覆盖其他的子节点,你可以使用 update() 函数:
var hopperRef = usersRef.child("gracehop");
hopperRef.update({
"nickname": "Amazing Grace"
});
这样会更新 Grace的数据,更新她的 nickname 。如果我们用 set 而不是 update ,date_of_birth 和 full_name 都会被删除。
保存一个列表
当多个用户同时试图在一个节点下新增一个子节点的时候,这时,数据就会被重写,覆盖。 为了解决这个问题,Wilddog push()采用了生成唯一ID 作为key的方式。通过这种方式,多个用户同时在一个节点下面push 数据,他们的key一定是不同的。这个key是通过一个基于时间戳和随机的算法生成的。wilddog采用了足够多的位数保证唯一性。
用户可以用push向博客app中写新内容:
var postsRef = ref.child("posts");
postsRef.push({
author: "gracehop",
title: "Announcing COBOL, a New Programming Language"
});
postsRef.push({
author: "alanisawesome",
title: "The Turing Machine"
});
产生的数据有一个唯一ID:
{
"posts": {
"-JRHTHaIs-jNPLXO": {
"author": "gracehop",
"title": "Announcing COBOL, a New Programming Language"
},
"-JRHTHaKuITFIhnj": {
"author": "alanisawesome",
"title": "The Turing Machine"
}
}
}
获取唯一ID调用push会返回一个引用,这个引用指向新增数据所在的节点。你可以通过调用 key() 来获取这个唯一ID
// 通过push()来获得一个新的数据库地址
var newPostRef = postsRef.push({
author: "gracehop",
title: "Announcing COBOL, a New Programming Language"
});
// 获取push()生成的唯一ID
var postID = newPostRef.key();
使用事务保存数据
当处理可能被并发更新导致损坏的复杂数据时,比如增量计数器,我们提供了事务操作。事务操作需要提供两个参数:一个更新函数和一个可选的完成 callback 函数。更新函数提供当前数据,当前数据是云端读取的。举例说明,如果我们想在一个的博文上计算点赞的数量,我们可以这样写一个事务:
var upvotesRef = new Wilddog('https://<appId>.wilddogio.com/android/saving-data/wildblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction(function (current_value) {
//
return (current_value || 0) + 1;
});
我们使用 currentData.getValue()!= null 来判断计数器是否为空或者是自增加。 如果上面的代码没有使用事务, 当两个客户端在同时试图累加,那结果可能是为数字 1 且非数字 2。
注意:doTransaction() 可能被多次被调用,必须处理 currentData 变量为 null 的情况。 当事务允许时,本地不会缓存从云端读取的数据。
更多关于事务 API 的文档。
4.获取数据
到目前为止,我们已经了解到如何向Wilddog中保存数据,现在我们来看看如何查询数据。
Wilddog查询数据的方式是绑定一个异步监听的回调函数,每当数据被第一次查询或者数据发生变化,这个回调函数都会被执行。
重新看博客app的例子,我们可以这样读取博客数据:
// 获得一个数据库连接实例
var ref = new Wilddog("https://<appId>.wilddogio.com/web/saving-data/blog/posts");
// 监听数据
ref.on("value", function(snapshot) {
console.log(snapshot.val());
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
});
运行这段代码,我们会看到控制台中打印出一个对象,包含所有的博客数据。每次当有新的博客数据被写入时,这个回调函数都会被触发执行。
回调函数接收一个Snapshot类型作为参数。一个Snapshot对象代表的是在指定的数据路径下,某一个时间点上数据的一个快照。调用它的val()函数,可以得到一个代表当前节点数据的javascript对象。如果路径节点下没有数据,那么这个Snapshot将为null。
这个例子中我们用 ‘value’ 作为事件类型,用于读取当前路径节点下的所有数据。我们还可以使用另外三种数据事件类型。
事件类型 WildDog提供了四种数据事件:value,child_added,child_changed,child_removed。
valuevalue 事件用来读取当前节点的静态数据快照, value 事件在初次获取到数据时被触发一次,此后每当数据发生变化都会被触发。回调函数被执行时候,当前节点下所有数据的静态快照会被作为参数传入。
child_added与value事件返回指定数据节点下的所有数据不同,child_added事件会为每一个现存的子节点数据触发一次,此后每当有子节点数据被增加时被触发一次。回调函数传入的也是DataSnapshot对象,包含的是新增子节点的数据。
如果我们只想获取新增的博客数据,可以使用 child_added事件:
// 获得一个数据库连接实例
var ref = new Wilddog("https://<appId>.wilddogio.com/web/saving-data/blog/posts");
// 获得新增加的数据
ref.on("child_added", function(snapshot) {
var newPost = snapshot.val();
console.log("Author: " + newPost.author);
console.log("Title: " + newPost.title);
});
child_changed当子节点数据发生了改变时,child_changed事件会被触发,事件回调函数中传入的参数是子节点改变后的数据快照。 我们可以使用child_changed事件来读取被修改的博客数据:
// 获得一个数据库连接实例
var ref = new Wilddog("https://<appId>.wilddogio.com/web/saving-data/blog/posts");
// 获得发生改变的数据
ref.on("child_changed", function(snapshot) {
var changedPost = snapshot.val();
console.log("The updated post title is " + changedPost.title);
});
child_removed当直接子节点被删除时,child_removed 事件被触发。事件回调函数中传入的参数是被删除的直接子节点的数据快照。
在博客的例子中,我们可以使用child_removed事件,在有博客数据被删除时,在控制台中打印出来:
// 获得一个数据库连接实例
var ref = new Wilddog("https://<appId>.wilddogio.com/web/saving-data/blog/posts");
// 获取被删除的数据
ref.on("child_removed", function(snapshot) {
var deletedPost = snapshot.val();
console.log("The blog post titled '" + deletedPost.title + "' has been deleted");
});
取消事件绑定
通过off()函数可以取消一个事件回调函数的绑定:
ref.off("value", originalCallback);
一次性读取数据
在某些场景下,也许需要事件的回调函数只被触发一次,然后立即取消。可以使用once()函数:
ref.once("value", function(data) {
// 执行业务处理,此回调函数只会被调用一次
})
查询数据
Wilddog支持选择性的查询数据。要构造一个查询,需要先指定数据如何排序,可以使用以下几个函数:orderByChild(),orderByKey(), orderByValue(), orderByPriority()。接下来,可以组合以下五个函数,构造出一个复杂的条件查询:limitToFirst(),limitToLast(),startAt(),endAt(),equalTo()。
下面我们来举例如何进行数据查询。假设现在有一些关于恐龙的数据如下:
{
"lambeosaurus": {
"height" : 2.1,
"length" : 12.5,
"weight": 5000
},
"stegosaurus": {
"height" : 4,
"length" : 9,
"weight" : 2500
}
}
按照指定的子节点排序通过将子节点的路径名作为参数传递给orderByKey(),可以实现按指定子节点排序。例如,要按照height进行排序,可以:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByChild("height").on("child_added", function(snapshot) {
console.log(snapshot.key() + " was " + snapshot.val().height + " meters tall");
});
不包含指定子节点的数据节点,将会按照该子节点为null进行排序,会排在最前边。
每个查询都只能有一个排序。在一个查询中多次调用orderByChild()会抛出错误。
按照数据节点名称排序使用orderByKey()函数,可以实现按照数据节点的名称进行排序。下面的例子按照alpha字母顺序读取所有的恐龙数据:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByKey().on("child_added", function(snapshot) {
console.log(snapshot.key());
});
按照数据节点的值排序使用orderByValue()函数,我们可以按照子节点的值进行排序。假设恐龙们进行了一场运动会,我们统计到它们的得分数据:
{
"scores": {
"bruhathkayosaurus" : 55,
"lambeosaurus" : 21,
"linhenykus" : 80,
"pterodactyl" : 93,
"stegosaurus" : 5,
"triceratops" : 22
}
}
要按照得分进行排序,我们可以构造一个这样的查询:
var ref = new Wilddog("https://<appId>.wilddogio.com/scores");
scoresRef.orderByValue().on("value", function(snapshot) {
snapshot.forEach(function(data) {
console.log("The " + data.key() + " dinosaur's score is " + data.val());
});
});
按照优先级排序关于优先级,请参考API文档中的相关部分。
复杂查询
我们已经了解到数据排序的函数。接下来是limit类的和range类的查询,通过它们,可以构建出更为复杂的查询:
limit查询limitToFirst()和limitToLast()两个函数用于设置最大多少个子节点数据会被同步。如果我们设置为100,那么初次获取到数据时,就只会最多触发100次事件回调函数。如果数据库中有少于100条消息数据,child_added事件将会为每一条消息数据被触发一次。如果消息数量多于100条,那么只有其中的100条会被触发child_added事件。如果我们使用了limitToFirst()函数,那么被触发的100条将会是排序最前边的100条。如果使用了limitToLast()函数,那么被触发的100条将是排序最后的100条。
继续恐龙的例子,我们可以获得体重最大的两种恐龙:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByChild("weight").limitToLast(2).on("child_added", function(snapshot) {
console.log(snapshot.key());
});
我们为child_added事件绑定的回调函数只会被执行2次。
同理,我们可以使用limitToFirst()函数查询最矮的两种恐龙:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByChild("height").limitToFirst(2).on("child_added", function(snapshot) {
console.log(snapshot.key());
});
我们也可以组合orderByValue()函数来使用limit类的查询。如果要构造出恐龙运动会得分的前3名,我们可以构造这样一个查询:
var ref = new Wilddog("https://<appId>.wilddogio.com/scores");
scoresRef.orderByValue().limitToLast(3).on("value", function(snapshot) {
snapshot.forEach(function(data) {
console.log("The " + data.key() + " dinosaur's score is " + data.val());
});
});
range查询使用startAt(),endAt(),和equalTo()函数,我们可以任意指定任意值的范围进行查询。例如,如果要查询所有至少3米高以上的恐龙,可以组合orderByChild()和startAt()查询:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByChild("height").startAt(3).on("child_added", function(snapshot) {
console.log(snapshot.key())
});
我们可以使用endAt()来查询按照字母排序,所有名字排在Pterodactyl之前的恐龙:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByKey().endAt("pterodactyl").on("child_added", function(snapshot) {
console.log(snapshot.key());
});
注意,startAt()和endAt()都是包含边界值的,也就是说“pterodactyl”符合上边的查询条件。
我们可以同时使用startAt()和endAt()来限定一个范围。下面的例子查询出所有名字以字母“b”开头的恐龙:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByKey().startAt("b").endAt("b~").on("child_added", function(snapshot) {
console.log(snapshot.key());
});
这个例子中使用的“~”符号是ASCII中的126字符。因为它排在所有常规的ASCII字符之后,所以这个查询匹配所有以b开头的值。
使用equalTo()函数,可以进行精准的查询。例如,查询所有的25米高的恐龙:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.orderByChild("height").equalTo(25).on("child_added", function(snapshot) {
console.log(snapshot.key());
});
同样的,startAt(),endAt()函数也可以和orderByValue()函数组合进行range查询。
总结组合这些函数,我们可以构造出各种复杂的查询。例如,要找出长度小于Stegosaurus但最接近的恐龙的名字:
var ref = new Wilddog("https://<appId>.wilddogio.com/dinosaurs");
ref.child("stegosaurus").child("height").on("value", function(stegosaurusHeightSnapshot) {
var favoriteDinoHeight = stegosaurusHeightSnapshot.val();
var queryRef = ref.orderByChild("height").endAt(favoriteDinoHeight).limitToLast(2)
queryRef.on("value", function(querySnapshot) {
if (querySnapshot.numChildren() == 2) {
// 数据是按照height字段的值升序排列的,所以我们需要取得第一个元素。
querySnapshot.forEach(function(dinoSnapshot) {
console.log("The dinosaur just shorter than the stegasaurus is " + dinoSnapshot.key());
// 返回true意味着我们只需要循环forEach()一次
return true;
});
} else {
console.log("The stegosaurus is the shortest dino");
}
});
});
数据排序
本小节介绍在使用各种排序方式时,数据究竟是如何排序的。
orderByChild
当使用orderByChild(key)时,按照子节点的公有属性key的value进行排序。仅当value为单一的数据类型时,排序有意义。如果key属性有多种数据类型时,排序不固定,此时不建议使用orderByChild(key)获取全量数据,例如,
{
"scores": {
"no1" : {
"name" : "tyrannosaurus",
"score" : "120"
},
"no2" : {
"name" : "bruhathkayosaurus",
"score" : 55
},
"no3" : {
"name" : "lambeosaurus",
"score" : 21
},
"no4" : {
"name" : "linhenykus",
"score" : 80
},
"no5" : {
"name" : "pterodactyl",
"score" : 93
},
"no6" : {
"name" : "stegosaurus",
"score" : 5
},
"no7" : {
"name" : "triceratops",
"score" : 22
},
"no8" : {
"name" : "brontosaurus",
"score" : true
}
}
}
霸王龙的分数是string类型,雷龙的分数是boolean类型,而其他恐龙的分数是numberic类型,此时使用orderByChild(key)获得全量数据时,是一个看似固定的排序结果;但是配合使用limitToFirst()时,将获得不确定的结果。Object类型数据的 value 值为 null,不会出现在结果中。 当配合使用startAt()、endAt()和equalTo()时,如果子节点的公有属性key包含多种数据类型,将按照这些函数的参数的类型排序,即只能返回这个类型的有序数据。上面的数据如果使用 orderByChild(‘score’).startAt(60).limitToFirst(4) 将得到下面的结果:
{
"no4" : {
"name" : "linhenykus",
"score" : 80
},
"no5" : {
"name" : "pterodactyl",
"score" : 93
}
}
注意:如果path与value的总长度超过1000字节时,使用orderByChild(key)将搜索不到该数据。
orderByKey当使用orderByKey()对数据进行排序时,数据将会按照下面的规则,以字段名升序排列返回。注意,节点名只能是字符串类型。
1, 节点名能转换为32-bit整数的子节点优先,按数值型升序排列。
2, 接下来是字符串类型的节点名,按字典序排列。
orderByValue
当使用orderByValue()时,按照直接子节点的 value 进行排序。仅当 value 为单一的数据类型时,排序有意义。如果子节点包含多种数据类型时,排序不固定,此时不建议使用orderByValue()获取全量数据,例如,
{
"scores": {
"tyrannosaurus" : "120",
"bruhathkayosaurus" : 55,
"lambeosaurus" : 21,
"linhenykus" : 80,
"pterodactyl" : 93,
"stegosaurus" : 5,
"triceratops" : 22,
"brontosaurus" : true
}
}
霸王龙的分数是 string类型,雷龙的分数是 boolean 类型,而其他恐龙的分数是 numberic 类型,此时使用 orderByValue() 获得全量数据时,是一个看似固定的排序结果;但是配合使用limitToFirst()时,将获得不确定的结果。Object类型数据的value值为null,不会出现在结果中。 当配合使用startAt()、endAt()和equalTo()时,如果子节点的value包含多种数据类型,将按照这些函数的参数的类型排序,即只能返回这个类型的有序数据。上面的数据如果使用orderByValue().startAt(60).limitToFirst(4)将得到下面的结果:
{
"linhenykus" : 80,
"pterodactyl" : 93
}
注意:如果path与value的总长度超过1000字节时,使用orderByValue()将搜索不到该数据。
orderByPriority当使用orderByPriority()对数据进行排序时,子节点数据将按照优先级和字段名进行排序。注意,优先级的值只能是数值型或字符串。
1, 没有优先级的数据(默认)优先。
2, 接下来是优先级为数值型的子节点。它们按照优先级数值排序,由小到大。
3, 接下来是优先级为字符串的子节点。它们按照优先级的字典序排列。
4, 当多个子节点拥有相同的优先级时(包括没有优先级的情况),它们按照节点名排序。节点名可以转换为数值类型的子节点优先(数值排序),接下来是剩余的子节点(字典序排列)。
5.组织数据
构造恰当的NoSQL存储结构需要事先考虑很多因素。最重要的是,必须要知道将来数据会被如何查询,如何存储数据才能使查询最方便。
避免层级过深
尽管可以使用JSON任意地组织数据,但不同的组织方式对读取性能的影响是很大的。Wilddog的工作方式是当你查询某个节点,Wilddog会返回这个节点下的所有子节点。所以,应该尽可能使数据扁平化,就像组织SQL关系型数据表一样。
我们不推荐这种实践
{
// 一个非常差的充满嵌套的数据结构。请勿模仿。
// 对"rooms"进行遍历查找来获得名字需要下载很多很多的messages。
"rooms": {
"one": {
"name": "room alpha",
"type": "private",
"messages": {
"m1": { "sender": "mchen", "message": "foo" },
"m2": { ... },
// 非常长的messages列表
}
}
}
}
对于这种嵌套存储的设计,很难遍历所有的数据。比如列出所有的rooms这样一个很简单的操作,也会查询整个rooms数据节点,返回所有的rooms下的数据节点到客户端。
使数据扁平化
如果数据分布到不同的路径下,那么就可以根据需要查询最小化的数据量,大大提高查询性能:
{
// rooms数据节点下仅包含房间的基本信息和唯一ID。
"rooms": {
"one": {
"name": "room alpha",
"type": "private"
},
"two": { ... },
"three": { ... }
},
//room成员可以很方便的的存取
"members": {
"one": {
"mchen": true,
"hmadi": true
},
"two": { ... },
"three": { ... }
},
//消息数据与其他数据分离开,这样我们在查询其他数据时就不收消息数据的影响,从而提升性能。
//消息数据可以通过room ID方便的分页和查询。
"messages": {
"one": {
"m1": { "sender": "mchen", "message": "foo" },
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
这样组织数据,就可以很方便的查询room列表了,只需要传输很少的字节数。message数据也可以很容易的查询。
使数据可扩展
很多时候需要查询一个列表的一个子集数据,尤其是当这个列表中包含多达数千条或更多记录时。当这个数据之间的关系是单向且数据比较稳定的时候,我们可以简单的把子节点数据嵌套到父节点之下:
{
"users": {
"john": {
"todoList": {
"rec1": "Walk the dog",
"rec2": "Buy milk",
"rec3": "Win a gold medal in the Olympics"
}
}
}
}
但很多时候数据频发变化,或者有时候必须把数据拆分存储到不同的路径下(John可能有一个长达数千项的todo列表)。通常可以用获取数据中介绍的函数来查询一个列表的子集。
但仅仅如此可能还是不够的。考虑一个例子,users和groups之间的双向关系。user可以属于group,group包含一个user列表。乍看之下数据可能这样组织:
{
"users": {
"mchen": { "name": "Mary Chen" },
"brinchen": { "name": "Byambyn Rinchen" },
"hmadi": { "name": "Hamadi Madi" }
},
"groups": {
"alpha": {
"name": "Alpha Tango",
"members": {
"m1": "mchen",
"m2": "brinchen",
"m3": "hamadi"
}
},
"bravo": { ... },
"charlie": { ... }
}
}
看起来不错。但是当需要判断一个user属于哪些group的时候,困难就来了。我们可以在数据发生改变的时候遍历并更新所有的group,但这样做成本很高,也很慢。更糟糕的是,如果Mary没有权限查看所有的group时怎么办呢?当查询整个列表时,会得到一个没有权限访问的错误。
我们需要的是一种优雅的方式,可以列出Mary属于哪些group,只需要查询这些group就行了。数据可以这样组织:
{
"users": {
"mchen": {
"name": "Mary Chen",
// 在Mary的数据下,建立他所属group的索引。
"groups": {
// 这里的值是什么并不重要。重要的是这个子节点的key存在。
"alpha": true,
"charlie": true
}
},
...
},
"groups": { ... }
}
我们把关系数据同时存储在了Mary的记录下和group数据下,这样造成了数据的重复。如果要把Mary从一个组中删除,就需要更新两个地方。
对于双向的关系来说,这样的冗余是有必要的。这样做使我们可以很高效的查询Mary的个人信息,即使users和groups都有百万级的数据,且规则表达式禁止访问不相关的数据时。
为什么我们把id作为key,而把value设置为true呢?这样做是有好处的。这样使得检查一个id是否存在变得非常简单,只需要读取/users/mchen/groups/$group_id,看它是否为null就可以了。
// 判断Mary是否属于alpha group
var ref = new Wilddog("https://<appId>.wilddogio.com/web/org/users/mchen/groups/alpha");
ref.once('value', function(snap) {
var result = snap.val() === null? 'is not' : 'is';
console.log('Mary ' + result + ' a member of alpha group');
});
6.了解安全
安全是一个非常重大的话题,通常也是app开发中最困难的部分之一。Wilddog使用一种声明式的规则表达式,对数据的访问权限进行配置,让这一切变得简单。
认证
用户ID是一个非常重要概念,不同的用户拥有不同的数据和不同的权限,比如,在一个聊天程序中,每一条消息都有它的发布者,用户可以删除自己的消息,而不能删除别人的。安全的第一步是用户认证。
Wilddog 提供了以下终端用户认证的方式:
- 集成微博,微信,QQ等社交平台的OAuth认证
- Email/密码登录,并且提供用户管理
- 自定义token,方便用户集成已有的用户账户系统。
授权
知道用户的身份只是安全的一部分,一旦你知道谁在访问数据,你需要一种方式来控制访问权限。Wilddog提供了一种声明式的表达式语言,你可以在控制面板中的“规则表达式”tab下进行编辑。这些规则表达式让你可以管理数据的访问规则。规则级联应用到其子节点。
{
"rules": {
"foo": {
".read": true,
".write": false
}
}
}
这个例子允许所有人访问数据节点 foo。
规则表达式包含一系列内置对象和函数。最重要的一个内置对象是auth,它在终端用户认证的时候生成,包含终端用户的信息和用户的唯一id:auth.uid。
auth对象是很多规则表达式的基础。
{
"rules": {
"users": {
"$user_id": {
".write": "$user_id == auth.uid"
}
}
}
}
这个规则保证了:只有终端用户的唯一id等于动态路径$user_id的值时,用户才能写入数据。
数据校验
规则表达式中还包含一个.validate规则,用于对数据进行校验,确保数据的格式正确。它的语法和.read与.write相同,不同的是.validate规则不会向下级联。
{
"rules": {
"foo": {
".validate": "newData.isString() && newData.val().length() < 100"
}
}
}
这一规则确保了在/foo/节点下,写入的数据必须是字符串类型,且必须长度小于100。
.validate规则可以使用的内置对象和函数与.read和.write相同。
{
"rules": {
"user": {
".validate": "auth != null && newData.val() == auth.uid"
}
}
}
这一规则强制使写入/user/下的数据必须是当前登陆用户的唯一id。
.validate规则并不是要彻底取消应用中的数据校验代码。为了获得更好的性能和用户体验,你仍然必须在应用代码中对数据进行校验。
了解更多
到此为止,你应该对Wilddog中的应用安全机制有了一个大体的了解。
规则表达式是复杂且强大的,本开发向导中只涵盖了非常小的一部分。更多关于规则表达式的细节,请参考规则表达式文档,这里将会讲述所有的内置函数和对象。
7.终端用户认证
绝大多数应用都需要一套终端用户账号体系。对终端用户进行唯一标识之后,才能对用户进行个性化的用户体验,控制用户对数据的访问权限。提供终端用户唯一标识的过程被称为终端用户认证。Wilddog为开发者提供了多种用户认证方式。
当一个终端用户进行认证之后,会有以下几件事情发生:
1. 终端用户的信息通过回调函数返回给了客户端。这样应用就可以得到用户的信息。
2. 返回的用户信息之中包含一个唯一标识uid,它是按照一定的策略生成的,确保在多种终端用户登陆方式中可以保持唯一。并且对于特定的终端用户,这个id永远不会发生改变。uid字段是一个字符串,它包含终端认证的方式,和这种认证方式返回的id,中间用冒号分隔。
3. 在规则表达式中,内置对象auth被定义。对于未进行终端认证的用户,auth对象为null。对于终证过的端认用户,auth对象包含终端用户的唯一标识(auth.uid),可能还包含其他用户信息数据。
设置终端认证方式
下面是wilddog支持的用户认证方式:
认证方式 | 描述 |
---|---|
自定义 | 你自行生成登陆token,通过这种方式,可以集成你现有的用户账号体系。也可以用于服务器端和wilddog云进行交互。 |
Email/Password | 由Wilddog对终端用户进行管理。终端用户通过Email和密码的方式注册和登录。 |
Anonymous | 使用匿名认证后,系统会为每个匿名用户生成唯一的标识。在会话过程中保持唯一标识不变。 |
新浪微博 | 使用新浪微博账户认证,只需要编写客户端代码即可。 |
微信 | 使用微信账户认证,只需要编写客户端代码即可。 |
使用QQ账户认证,只需要编写客户端代码即可。 |
配置终端用户认证
出于安全的原因,如果你使用的是基于web的OAuth认证流程(新浪微博,微信,QQ),只有在你设置的白名单中的域名才可以进行终端用户认证。为了开发和测试的方便,所有Wilddog应用默认都将localhost和127.0.0.1加入了白名单。如果你将应用部署在其它的域名下,你需要将域名添加到白名单中。
1,进入应用的控制面板。
2,打开终端用户认证。
3,点击“点击设置白名单”链接,并将域名设置到“OAuth 跳转域名白名单”中。
启动终端用户认证
1,进入控制面板。
2,选择自己Wilddog App。
3,点击终端用户认证。
4,选中一个认证方式。
5,选择以各种认证方式,并启用。
6,如果你使用第三方社交平台帐号登录。需要将社交平台的 CLIENT ID 和 Secret 添加到表单中。
7,如果使用微博帐号登录, 需要在微博开发平台配置会调地址。 在微博开发平台,打开“我的应用”。点击“我的应用”,在左侧的菜单栏选中“接口管理” ->“授权机制”,修改“授权回调页面”为https://auth.wilddog.com/v1/[WILDDOG-APPID]/auth/weibo/callback (注: 更换[WILDDOG-APPID]为你的Wilddog App Id) 。
8,如果使用QQ帐号登录, 需要在腾讯开发平台配置回调域名。 在QQ互联,打开“管理中心”菜单,在网站列表中选中应用。点击操作按钮,查看详情,在“我的应用”中点击“管理网页应用”。在“基本信息”的“回调地址”,填写 https://auth.wilddog.com/v1/[WILDDOG-APPID]/auth/qq/callback (注: 更换[WILDDOG-APPID]为你的Wilddog App Id)。
9,如果使用微信帐号登录,需要在微信.开放平台配置回调域名。 在微信.开放平台,打开“管理中心”, 选择“网站应用”。选中相关的应用,点击“查看”。在“网站信息”中配置回调域名 auth.wilddog.com。
10,如果在微信App里使用微信帐号登录,需要在微信公众帐号配置回调域名。 在微信.公众平台,登陆公众平台。在左侧导航栏,选中“开发者中心”,在“接口权限表”中找到“网页服务” -> “网页账号” -> “网页授权获取用户基本信息”后,点击“修改”,填写“授权回调页面域名” auth.wilddog.com。
监控终端用户的认证状态
使用onAuth()函数,对终端用户的认证状态的改变进行监听。
// 编写一个回调函数
function authDataCallback(authData) {
if (authData) {
console.log("User " + authData.uid + " is logged in with " + authData.provider);
} else {
console.log("User is logged out");
}
}
// 注册回调函数,在每次终端用户认证状态发生改变时,回调函数被执行。
var ref = new Wilddog("https://<appId>.wilddogio.com");
ref.onAuth(authDataCallback);
如果要停止对终端用户状态改变的监听,可以用offAuth()函数:
ref.offAuth(authDataCallback);
可以使用getAuth()函数检查终端用户认证状态。
var ref = new Wilddog("https://<appId>.wilddogio.com");
var authData = ref.getAuth();
if (authData) {
console.log("User " + authData.uid + " is logged in with " + authData.provider);
} else {
console.log("User is logged out");
}
终端用户登录
根据终端认证方式的不同,这些接口接收的参数不同,但它们都有类似的签名,接受回调函数。
// 创建一个回调来处理终端用户认证的结果
function authHandler(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
console.log("Authenticated successfully with payload:", authData);
}
}
// 通过一个自定义的Wilddog Token来认证用户
ref.authWithCustomToken("<token>", authHandler);
// 或者使用email/password认证方式。
ref.authWithPassword({
email : 'Loki@asgard.com',
password : 'dwadwadc'
}, authHandler);
// 或者弹出OAuth认证,比如新浪微博
ref.authWithOAuthPopup("weibo", authHandler);
ref.authWithOAuthRedirect("weibo", authHandler);
终端用户认证生成的token有效期为24小时。
终端用户登出
使用unauth()函数,可以使终端用户的token失效。用户将退出系统。
ref.unauth();
用弹出窗口和重定向的方式进行第三方用户认证
Wilddog的OAuth认证支持三种不同的方式:弹窗、浏览器重定向、OAuth token登录。
大多数浏览器阻止javascript弹出窗口,除非是用户行为触发的。因此,我们应该只在用户点击事件处理中调用authWithOAuthPopup()函数。
注意:浏览器的弹窗和重定向并不是在所有的浏览器环境下都可用。弹窗在iOS版的Chrome下,iOS的预览面板,和本地file://的url下不可用。因此,建议组合使用多种认证方式,确保任何环境下都没有问题
var ref = new Wilddog("https://<appId>.wilddogio.com");
// 优先选择弹窗方式,这样我们就不需要重定向网页了
ref.authWithOAuthPopup("weibo", function(error, authData) {
if (error) {
if (error.code === "TRANSPORT_UNAVAILABLE") {
// 回退到浏览器重定向,当我们返回到原始页面的时候自动获取session。
ref.authWithOAuthRedirect("weibo", function(error) { /* ... */ });
}
} else if (authData) {
// 使用Wilddog的用户认证
}
});
错误处理
调用的认证函数时,传入一个回调函数。这个函数被执行的时候,根据认证的结果,会传入一个error和authData对象作为参数。
所有error对象都至少包含一个code字段和一个message字段。有时还会有一些附加信息放在一个details字段中,例如:
{
code: "TRANSPORT_UNAVAILABLE",
message: "There are no login transports available for the requested method.",
details: "More details about the specific error here."
}
有时候你需要根据特定的错误信息对用户进行提示。例如使用Email/Password认证方式时,你需要判断是否Email或密码错误:
var ref = new Wilddog("https://<appId>.wilddogio.com");
ref.authWithPassword({
email : 'Loki@asgard.com',
password : 'dwadwa'
}, function(error, authData) {
if (error) {
switch (error.code) {
case "INVALID_EMAIL":
console.log("The specified user account email is invalid.");
break;
default:
console.log("Error logging user in:", error);
}
} else {
console.log("Authenticated successfully with payload:", authData);
}
});
错误列表
Error Code | Description |
---|---|
AUTHENTICATION_DISABLED | 指定的认证方式在当前Wilddog应用下被禁用。 |
EMAIL_TAKEN | 无法注册用户,因为Email已经被使用。 |
INVALID_ARGUMENTS | 参数错误。 |
INVALID_CREDENTIALS | 提供用于OAuth认证的credential错误。可能是格式错误或已过期。 |
INVALID_EMAIL | Email地址或密码不合法。 |
INVALID_ORIGIN | 请求的来源域名不在白名单中。这是一个安全错误。 |
PROVIDER_ERROR | 第三方平台错误。 |
UNKNOWN_ERROR | 未知错误。 |
.
8.离线功能
离线行为
当暂时失去网络连接的时候,Wilddog应用仍然能正常工作。Wilddog应用的每个客户端都维护着自己的数据副本。更新数据的时候,写入的是本地副本,然后Wilddog客户端将数据同步到云端和其它客户端,这个过程遵循的是”best-effort”原则。
所有的数据更新操作会立即触发本地事件,在数据被同步到云端之前。这样就保障了在网络存在延迟或连接暂时中断的条件下,应用仍然能及时响应并正常工作。
一旦网络连接恢复,数据将会和云端保持同步。
离线事件
在实时应用中,检测客户端是否断线是非常有必要的。例如,当一个用户的网络连接中断时,我们希望标记这个用户“离线”状态。
Wilddog提供了离线事件功能,使得客户端连接断开时,指定的数据被写入云数据库中。不论是客户端主动断开,还是意外的网络中断,甚至是客户端应用崩溃,这些数据写入动作都将会被执行。因此我们可以依靠这个功能,在用户离线的时候,做一些数据清理工作。Wilddog支持的所有数据写入动作,包括set, update,remove,都可以设置在离线事件中执行。
下面是一个例子,使用onDisconnect()函数,在离线的时候写入数据:
var presenceRef = new Wilddog('https://<appId>.wilddogio.com/disconnectmessage');
// 当客户端连接中断时,写入一个字符串
presenceRef.onDisconnect().set("I disconnected!");
离线事件是如何工作的当进行了一个onDisconnect()调用之后,这个事件将会被记录在云端。云端会监控每一个客户端的连接。如果发生了超时,或者客户端主动断开连接,云端就触发记录的离线事件。
客户端可以通过回调函数,确保离线事件被云端正确记录了:
presenceRef.onDisconnect().remove( function(err) {
if( err ) {
console.error('could not establish onDisconnect event', err);
}
});
要取消一个离线事件,可以使用cancel()函数:
var onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set('I disconnected');
// 要取消离线事件
onDisconnectRef.cancel();
检查连接状态
在许多应用场景下,客户端需要知道自己是否在线。Wilddog客户端提供了一个特殊的数据地址:/.info/connected。每当客户端的连接状态发生改变时,这个地址的数据都会被更新。
var connectedRef = new Wilddog("https://<appId>.wilddogio.com/.info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
/.info/connected的值是boolean类型的,它不会和云端进行同步。
时间问题
云端时间戳Wilddog提供了一种将云端时间戳作为数据写入的机制。这个机制和onDisconnect()函数组合起来,很容易实现记录客户端断线事件的功能:
var userLastOnlineRef = new Wilddog("https://<appId>.wilddogio.com/users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(Wilddog.ServerValue.TIMESTAMP);