[TOC]
## 數據存儲
數據存儲(LeanStorage)是 LeanCloud 提供的核心功能之一,它的使用方法與傳統的關系型數據庫有諸多不同。下面我們將其與傳統數據庫的使用方法進行對比,讓大家有一個初步了解。
下面這條 SQL 語句在絕大數的關系型數據庫都可以執行,其結果是在 Todo 表里增加一條新數據:
~~~
INSERT INTO Todo (title, content) VALUES ('工程師周會', '每周工程師會議,周一下午 2 點')
~~~
使用傳統的關系型數據庫作為應用的數據源幾乎無法避免以下步驟:
0
* 插入數據之前一定要先創建一個表結構,并且隨著之后需求的變化,開發者需要不停地修改數據庫的表結構,維護表數據。
* 每次插入數據的時候,客戶端都需要連接數據庫來執行數據的增刪改查(CRUD)操作。
使用 LeanStorage,實現代碼如下:
~~~
// 聲明一個 Todo 類型
var Todo = AV.Object.extend('Todo');
// 新建一個 Todo 對象
var todo = new Todo();
todo.set('title', '工程師周會');
todo.set('content', '每周工程師會議,周一下午2點');
todo.save().then(function (todo) {
// 成功保存之后,執行其他邏輯.
console.log('New object created with objectId: ' + todo.id);
}, function (error) {
// 異常處理
console.error('Failed to create new object, with error message: ' + error.message);
});
~~~
使用 LeanStorage 的特點在于:
* 不需要單獨維護表結構。例如,為上面的 Todo 表新增一個?`location`?字段,用來表示日程安排的地點,那么剛才的代碼只需做如下變動:
~~~
var Todo = AV.Object.extend('Todo');
var todo = new Todo();
todo.set('title', '工程師周會');
todo.set('content', '每周工程師會議,周一下午2點');
// 只要添加這一行代碼,服務端就會自動添加這個字段
todo.set('location','會議室');
todo.save().then(function (todo) {
// 成功保存之后,執行其他邏輯.
}, function (error) {
// 異常處理
});
~~~
* 數據可以隨用隨加,這是一種無模式化(Schema Free)的存儲方式。
* 所有對數據的操作請求都通過 HTTPS 訪問標準的 REST API 來實現。
* 我們為各個平臺或者語言開發的 SDK 在底層都是調用統一的 REST API,并提供完整的接口對數據進行增刪改查。
LeanStorage 在結構化數據存儲方面,與 DB 的區別在于:
1. Schema Free/Not free 的差異;
2. 數據接口上,LeanStorage 是面向對象的(數據操作接口都是基于 Object 的),開放的(所有移動端都可以直接訪問),DB 是面向結構的,封閉的(一般在 Server 內部訪問);
3. 數據之間關聯的方式,DB 是主鍵外鍵模型,LeanStorage 則有自己的關系模型(Pointer、Relation 等);
LeanStorage 支持兩種存儲類型:
* 對象
* 文件
我們將按照順序逐一介紹各類的使用方法。
## [SDK 安裝](#SDK_安裝)
請閱讀?[JavaScript 安裝指南](https://leancloud.cn/docs/sdk_setup-js.html)。
## [Web 安全](#Web_安全)
如果在前端使用 JavaScript SDK,當你打算正式發布的時候,請務必配置?Web 安全域名。配置方式為:進入?[控制臺 / 設置 / 安全中心 /?Web 安全域名](https://leancloud.cn/app.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/security)。這樣就可以防止其他人,通過外網其他地址盜用你的服務器資源。
具體安全相關內容可以仔細閱讀文檔?[數據和安全](https://leancloud.cn/docs/data_security.html)?。
## [對象](#對象)
`AV.Object`?是 LeanStorage 對復雜對象的封裝,每個?`AV.Object`?包含若干屬性值對,也稱鍵值對(key-value)。屬性的值是與 JSON 格式兼容的數據。通過 REST API 保存對象需要將對象的數據通過 JSON 來編碼。這個數據是無模式化的(Schema Free),這意味著你不需要提前標注每個對象上有哪些 key,你只需要隨意設置 key-value 對就可以,云端會保存它。
### [數據類型](#數據類型)
`AV.Object`?支持以下數據類型:
~~~
// 該語句應該只聲明一次
var TestObject = AV.Object.extend('DataTypeTest');
var number = 2014;
var string = 'famous film name is ' + number;
var date = new Date();
var array = [string, number];
var object = { number: number, string: string };
var testObject = new TestObject();
testObject.set('testNumber', number);
testObject.set('testString', string);
testObject.set('testDate', date);
testObject.set('testArray', array);
testObject.set('testObject', object);
testObject.set('testNull', null);
testObject.save().then(function(testObject) {
// 成功
}, function(error) {
// 失敗
});
~~~
我們不推薦在?`AV.Object`?中儲存大塊的二進制數據,比如圖片或整個文件。每個?`AV.Object`?的大小都不應超過 128 KB。如果需要儲存更多的數據,建議使用?[`AV.File`](#文件)。
若想了解更多有關 LeanStorage 如何解析處理數據的信息,請查看專題文檔《[數據與安全](https://leancloud.cn/docs/data_security.html)》。
### [構建對象](#構建對象)
構建一個?`AV.Object`?可以使用如下方式:
~~~
// AV.Object.extend('className') 所需的參數 className 則表示對應的表名
// 聲明一個類型
var Todo = AV.Object.extend('Todo');
~~~
注意:如果你的應用時不時出現?`Maximum call stack size exceeded`?異常,可能是因為在循環或回調中調用了?`AV.Object.extend`。有兩種方法可以避免這種異常:
* 升級 SDK 到 v1.4.0 或以上版本
* 在循環或回調外聲明 Class,確保不會對一個 Class 執行多次?`AV.Object.extend`
從 v1.4.0 開始,SDK 支持使用 ES6 中的 extends 語法來聲明一個繼承自?`AV.Object`?的類,上述的 Todo 聲明也可以寫作:
~~~
class Todo extends AV.Object {}
// 需要向 SDK 注冊這個 Class
AV.Object.register(Todo);
~~~
每個 id 必須有一個 Class 類名稱,這樣云端才知道它的數據歸屬于哪張數據表。
### [保存對象](#保存對象)
現在我們保存一個?`TodoFolder`,它可以包含多個 Todo,類似于給行程按文件夾的方式分組。我們并不需要提前去后臺創建這個名為?TodoFolder?的 Class 類,而僅需要執行如下代碼,云端就會自動創建這個類:
~~~
// 聲明類型
var TodoFolder = AV.Object.extend('TodoFolder');
// 新建對象
var todoFolder = new TodoFolder();
// 設置名稱
todoFolder.set('name','工作');
// 設置優先級
todoFolder.set('priority',1);
todoFolder.save().then(function (todo) {
console.log('objectId is ' + todo.id);
}, function (error) {
console.error(error);
});
~~~
創建完成后,打開?[控制臺 > 存儲](https://leancloud.cn/data.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/),點開?`TodoFolder`?類,就可以看到剛才添加的數據。除了 name、priority(優先級)之外,其他字段都是數據表的內置屬性。
| 內置屬性 | 類型 | 描述 |
| --- | --- | --- |
| `id` | String | 該對象唯一的 Id 標識 |
| `ACL` | ACL | 該對象的權限控制,實際上是一個 JSON 對象,控制臺做了展現優化。 |
| `createdAt` | Date | 該對象被創建的 UTC 時間,控制臺做了針對當地時間的展現優化。 |
| `updatedAt` | Date | 該對象最后一次被修改的時間 |
屬性名
也叫鍵或 key,必須是由字母、數字或下劃線組成的字符串。
自定義的屬性名,不能以?`__`(雙下劃線)開頭,也不能與內置屬性重名(不區分大小寫)。
屬性值
可以是字符串、數字、布爾值、數組或字典。
為提高代碼的可讀性和可維護性,建議使用駝峰式命名法(CamelCase)為類和屬性來取名。類,采用大駝峰法,如?`CustomData`。屬性,采用小駝峰法,如?`imageUrl`。
#### [使用 CQL 語法保存對象](#使用_CQL_語法保存對象)
LeanStorage 提供了類似 SQL 語法中的 Insert 方式保存一個對象,例如保存一個 TodoFolder 對象可以使用下面的代碼:
~~~
// 執行 CQL 語句實現新增一個 TodoFolder 對象
AV.Query.doCloudQuery('insert into TodoFolder(name, priority) values("工作", 1)').then(function (data) {
// data 中的 results 是本次查詢返回的結果,AV.Object 實例列表
var results = data.results;
}, function (error) {
//查詢失敗,查看 error
console.error(error);
});
~~~
#### [保存選項](#保存選項)
`AV.Object`?對象在保存時可以設置選項來快捷完成關聯操作,可用的選項屬性有:
| 選項 | 類型 | 說明 |
| --- | --- | --- |
| `fetchWhenSave` | BOOL | 對象成功保存后,自動返回本地已改動屬性在云端的最新值。用途請參考?[更新計數器](#更新計數器)。 |
| `query` | `AV.Query` | 當 query 中的條件滿足后對象才能成功保存,否則放棄保存,并返回錯誤碼 305。
開發者原本可以通過?`AV.Query`?和?`AV.Object`?分兩步來實現這樣的邏輯,但如此一來無法保證操作的原子性從而導致并發問題。該選項可以用來判斷多用戶更新同一對象數據時可能引發的沖突。 |
【query 選項舉例】用戶的賬務賬戶表?`Account`?有一個余額字段?`balance`,同時有多個請求要修改該字段值,為避免余額出現負值,只有滿足?balance >= 當前請求的數值?這個條件才允許修改,否則提示「余額不足,操作失敗!」。
~~~
var Account = AV.Object.extend('Account');
new AV.Query(Account).first().then(function(account) {
var amount = -100;
account.increment('balance', amount);
// 如果使用 JS SDK 2.0 以前的版本,save() 要加傳 null
// 作為第一個參數,否則會報錯:
// return account.save(null, {
return account.save({
query: new AV.Query(Account).greaterThanOrEqualTo('balance', -amount),
fetchWhenSave: true,
});
}).then(function(account) {
// 保存成功
console.log('當前余額為:', account.get('balance'));
}).catch(function(error) {
if (error.code === 305) {
console.log('余額不足,操作失敗!');
}
});
~~~
### [獲取對象](#獲取對象)
每個被成功保存在云端的對象會有一個唯一的 Id 標識?`id`,因此獲取對象的最基本的方法就是根據?`id`?來查詢:
~~~
var query = new AV.Query('Todo');
query.get('57328ca079bc44005c2472d0').then(function (todo) {
// 成功獲得實例
// data 就是 id 為 57328ca079bc44005c2472d0 的 Todo 對象實例
}, function (error) {
// 異常處理
});
~~~
除了使用?`AV.Query`,還可以采用在本地構建一個?`AV.Object`?的方式,通過接口和 objectId 把數據從云端拉取到本地:
~~~
// 第一個參數是 className,第二個參數是 objectId
var todo = AV.Object.createWithoutData('Todo', '5745557f71cfe40068c6abe0');
todo.fetch().then(function () {
var title = todo.get('title');// 讀取 title
var content = todo.get('content');// 讀取 content
}, function (error) {
// 異常處理
});
~~~
#### [獲取 objectId](#獲取_objectId)
每一次對象存儲成功之后,云端都會返回?`id`,它是一個全局唯一的屬性。
~~~
var todo = new Todo();
todo.set('title', '工程師周會');
todo.set('content', '每周工程師會議,周一下午2點');
todo.save().then(function (todo) {
// 成功保存之后,執行其他邏輯
// 獲取 objectId
var objectId = todo.id;
}, function (error) {
// 異常處理
});
~~~
#### [訪問對象的屬性](#訪問對象的屬性)
訪問 Todo 的屬性的方式為:
~~~
var query = new AV.Query('Todo');
query.get('558e20cbe4b060308e3eb36c').then(function (todo) {
// 成功獲得實例
// todo 就是 id 為 558e20cbe4b060308e3eb36c 的 Todo 對象實例
var priority = todo.get('priority');
var location = todo.get('location');
var title = todo.get('title');
var content = todo.get('content');
// 獲取三個特殊屬性
var objectId = todo.id;
var updatedAt = todo.updatedAt;
var createdAt = todo.createdAt;
//Wed May 11 2016 09:36:32 GMT+0800 (CST)
console.log(createdAt);
}, function (error) {
// 異常處理
console.error(error);
});
~~~
請注意以上代碼中訪問三個特殊屬性?`id`、`createdAt`、`updatedAt`?的方式。
如果訪問了并不存在的屬性,SDK 并不會拋出異常,而是會返回空值。
#### [默認屬性](#默認屬性)
默認屬性是所有對象都會擁有的屬性,它包括?`id`、`createdAt`、`updatedAt`。
0
`createdAt`
對象第一次保存到云端的時間戳。該時間一旦被云端創建,在之后的操作中就不會被修改。它采用國際標準時區 UTC,開發者可能需要根據客戶端當前的時區做轉化。
`updatedAt`
對象最后一次被修改(或最近一次被更新)的時間。
#### [同步對象](#同步對象)
多終端共享一個數據時,為了確保當前客戶端拿到的對象數據是最新的,可以調用刷新接口來確保本地數據與云端的同步:
~~~
// 使用已知 objectId 構建一個 AV.Object
var todo = new Todo();
todo.id = '5590cdfde4b00f7adb5860c8';
todo.fetch().then(function (todo) {
// // todo 是從服務器加載到本地的 Todo 對象
var priority = todo.get('priority');
}, function (error) {
});
~~~
在更新對象操作后,對象本地的?`updatedAt`?字段(最后更新時間)會被刷新,直到下一次 save 或 fetch 操作,`updatedAt`?的最新值才會被同步到云端,這樣做是為了減少網絡流量傳輸。
#### [同步指定屬性](#同步指定屬性)
目前 Todo 這個類已有四個自定義屬性:`priority`、`content`、`location`?和?`title`。為了節省流量,現在只想刷新?`priority`?和?`location`?可以使用如下方式:
~~~
// 使用已知 objectId 構建一個 AV.Object
var todo = new Todo();
todo.id = '5590cdfde4b00f7adb5860c8';
todo.fetch({
include:'priority,location'
}).then(function (todo) {
// 獲取到本地
}, function (error) {
// 異常處理
console.error(error);
});
~~~
刷新操作會強行使用云端的屬性值覆蓋本地的屬性。因此如果本地有屬性修改,刷新操作會丟棄這些修改。
### [更新對象](#更新對象)
LeanStorage 上的更新對象都是針對單個對象,云端會根據有沒有 objectId?來決定是新增還是更新一個對象。
假如?`id`?已知,則可以通過如下接口從本地構建一個?`AV.Object`?來更新這個對象:
~~~
// 第一個參數是 className,第二個參數是 objectId
var todo = AV.Object.createWithoutData('Todo', '5745557f71cfe40068c6abe0');
// 修改屬性
todo.set('content', '每周工程師會議,本周改為周三下午3點半。');
// 保存到云端
todo.save();
~~~
更新操作是覆蓋式的,云端會根據最后一次提交到服務器的有效請求來更新數據。更新是字段級別的操作,未更新的字段不會產生變動,這一點請不用擔心。
1
#### [使用 CQL 語法更新對象](#使用_CQL_語法更新對象)
LeanStorage 提供了類似 SQL 語法中的 Update 方式更新一個對象,例如更新一個 TodoFolder 對象可以使用下面的代碼:
~~~
// 執行 CQL 語句實現更新一個 TodoFolder 對象
AV.Query.doCloudQuery('update TodoFolder set name="家庭" where objectId="558e20cbe4b060308e3eb36c"')
.then(function (data) {
// data 中的 results 是本次查詢返回的結果,AV.Object 實例列表
var results = data.results;
}, function (error) {
// 異常處理
console.error(error);
});
~~~
#### [更新計數器](#更新計數器)
這是原子操作(Atomic Operation)的一種。 為了存儲一個整型的數據,LeanStorage 提供對任何數字字段進行原子增加(或者減少)的功能。比如一條微博,我們需要記錄有多少人喜歡或者轉發了它,但可能很多次喜歡都是同時發生的。如果在每個客戶端都直接把它們讀到的計數值增加之后再寫回去,那么極容易引發沖突和覆蓋,導致最終結果不準。此時就需要使用這類原子操作來實現計數器。
假如,現在增加一個記錄查看 Todo 次數的功能,一些與他人共享的 Todo 如果不用原子操作的接口,很有可能會造成統計數據不準確,可以使用如下代碼實現這個需求:
~~~
var todo = AV.Object.createWithoutData('Todo', '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 值
}, function (error) {
// 異常處理
});
~~~
#### [更新數組](#更新數組)
更新數組也是原子操作。使用以下方法可以方便地維護數組類型的數據:
* `AV.Object.add('arrayKey', value)`
將指定對象附加到數組末尾。
* `AV.Object.addUnique('arrayKey', value);`
如果數組中不包含指定對象,將該對象加入數組,對象的插入位置是隨機的。
* `AV.Object.remove('arrayKey', 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 = new AV.Object('Todo');
// 指定 reminders 是做一個 Date 對象數組
todo.addUnique('reminders', reminders);
todo.save().then(function (todo) {
console.log(todo.id);
}, function (error) {
// 異常處理
console.error(error);
});
~~~
### [刪除對象](#刪除對象)
假如某一個 Todo 完成了,用戶想要刪除這個 Todo 對象,可以如下操作:
~~~
var todo = AV.Object.createWithoutData('Todo', '57328ca079bc44005c2472d0');
todo.destroy().then(function (success) {
// 刪除成功
}, function (error) {
// 刪除失敗
});
~~~
刪除對象是一個較為敏感的操作。在控制臺創建對象的時候,默認開啟了權限保護,關于這部分的內容請閱讀《[JavaScript 權限管理使用指南](https://leancloud.cn/docs/acl_guide-js.html)》。
#### [使用 CQL 語法刪除對象](#使用_CQL_語法刪除對象)
LeanStorage 提供了類似 SQL 語法中的 Delete 方式刪除一個對象,例如刪除一個 Todo 對象可以使用下面的代碼:
~~~
// 執行 CQL 語句實現刪除一個 Todo 對象
AV.Query.doCloudQuery('delete from Todo where objectId="558e20cbe4b060308e3eb36c"').then(function () {
// 刪除成功
}, function (error) {
// 異常處理
});
~~~
### [批量操作](#批量操作)
為了減少網絡交互的次數太多帶來的時間浪費,你可以在一個請求中對多個對象進行創建、更新、刪除、獲取。接口都在?`AV.Object`?這個類下面:
~~~
var objects = []; // 構建一個本地的 AV.Object 對象數組
// 批量創建(更新)
AV.Object.saveAll(objects).then(function (objects) {
// 成功
}, function (error) {
// 異常處理
});
// 批量刪除
AV.Object.destroyAll(objects).then(function () {
// 成功
}, function (error) {
// 異常處理
});
// 批量獲取
AV.Object.fetchAll(objects).then(function (objects) {
// 成功
}, function (error) {
// 異常處理
});
~~~
批量設置 Todo 已經完成:
~~~
var query = new AV.Query('Todo');
query.find().then(function (todos) {
todos.map(function(todo) {
todo['status'] = 1;
});
return AV.Object.saveAll(todos);
}).then(function(todos) {
// 更新成功
}, function (error) {
// 異常處理
});
~~~
不同類型的批量操作所引發不同數量的 API 調用,具體請參考?[API 調用次數的計算](https://leancloud.cn/docs/faq.html#API_調用次數的計算)。
### [關聯數據](#關聯數據)
#### [`AV.Relation`](#AV_Relation)
對關聯數據進行查詢、排序等復雜操作,建議使用?[關聯表](https://leancloud.cn/docs/relation_guide-js.html#使用關聯表實現多對多關系_推薦_)?來構建對象之間的關系。
對象可以與其他對象相聯系。如前面所述,我們可以把一個?`AV.Object`?的實例 A,當成另一個?`AV.Object`?實例 B 的屬性值保存起來。這可以解決數據之間一對一或者一對多的關系映射,就像關系型數據庫中的主外鍵關系一樣。
例如,一個 TodoFolder 包含多個 Todo ,可以用如下代碼實現:
5
~~~
var todoFolder = new AV.Object('TodoFolder');
todoFolder.set('name', '工作');
todoFolder.set('priority', 1);
var todo1 = new AV.Object('Todo');
todo1.set('title', '工程師周會');
todo1.set('content', '每周工程師會議,周一下午2點');
todo1.set('location', '會議室');
var todo2 = new AV.Object('Todo');
todo2.set('title', '維護文檔');
todo2.set('content', '每天 16:00 到 18:00 定期維護文檔');
todo2.set('location', '當前工位');
var todo3 = new AV.Object('Todo');
todo3.set('title', '發布 SDK');
todo3.set('content', '每周一下午 15:00');
todo3.set('location', 'SA 工位');
var todos = [todo1, todo2, todo3];
AV.Object.saveAll(todos).then(function () {
var relation = todoFolder.relation('containedTodos'); // 創建 AV.Relation
todos.map(relation.add);
return todoFolder.save();// 保存到云端
}).then(function(todoFolder) {
// 保存成功
}), function (error) {
// 異常處理
});
~~~
#### [Pointer](#Pointer)
Pointer 只是個描述并沒有具象的類與之對應,它與?`AV.Relation`?不一樣的地方在于:`AV.Relation`?是在一對多的「一」這一方(上述代碼中的一指 TodoFolder)保存一個?`AV.Relation`?屬性,這個屬性實際上保存的是對被關聯數據多的這一方(上述代碼中這個多指 Todo)的一個 Pointer 的集合。而反過來,LeanStorage 也支持在「多」的這一方保存一個指向「一」的這一方的 Pointer,這樣也可以實現一對多的關系。
簡單的說, Pointer 就是一個外鍵的指針,只是在 LeanCloud 控制臺做了顯示優化。
現在有一個新的需求:用戶可以分享自己的 TodoFolder 到廣場上,而其他用戶看見可以給與評論,比如某玩家分享了自己想買的游戲列表(TodoFolder 包含多個游戲名字),而我們用 Comment 對象來保存其他用戶的評論以及是否點贊等相關信息,代碼如下:
~~~
var comment = new AV.Object('Comment');// 構建 Comment 對象
comment.set('likes', 1);// 如果點了贊就是 1,而點了不喜歡則為 -1,沒有做任何操作就是默認的 0
comment.set('content', '這個太贊了!樓主,我也要這些游戲,咱們團購么?');
// 假設已知被分享的該 TodoFolder 的 objectId 是 5735aae7c4c9710060fbe8b0
var targetTodoFolder = AV.Object.createWithoutData('TodoFolder', '5735aae7c4c9710060fbe8b0');
comment.set('targetTodoFolder', targetTodoFolder);
comment.save();//保存到云端
~~~
相關內容可參考?[關聯數據查詢](#AV.Relation_查詢)。
#### [地理位置](#地理位置)
地理位置是一個特殊的數據類型,LeanStorage 封裝了?`AV.GeoPoint`?來實現存儲以及相關的查詢。
首先要創建一個?`AV.GeoPoint`?對象。例如,創建一個北緯 39.9 度、東經 116.4 度的?`AV.GeoPoint`?對象(LeanCloud 北京辦公室所在地):
~~~
// 第一個參數是: latitude ,緯度
// 第二個參數是: longitude,經度
var point = new AV.GeoPoint(39.9, 116.4);
// 以下是創建 AV.GeoPoint 對象不同的方法
var point2 = new AV.GeoPoint([12.7, 72.2]);
var point3 = new AV.GeoPoint({ latitude: 30, longitude: 30 });
~~~
假如,添加一條 Todo 的時候為該 Todo 添加一個地理位置信息,以表示創建時所在的位置:
~~~
todo.set('whereCreated', point);
~~~
同時請參考?[地理位置查詢](#地理位置查詢)。
### [數據協議](#數據協議)
很多開發者在使用 LeanStorage 初期都會產生疑惑:客戶端的數據類型是如何被云端識別的? 因此,我們有必要重點介紹一下 LeanStorage 的數據協議。
先從一個簡單的日期類型入手,比如在 JavaScript 中,默認的日期類型是?`Date`,下面會詳細講解一個?`Date`?是如何被云端正確的按照日期格式存儲的。
為一個普通的?`AV.Object`?的設置一個?`Date`?的屬性,然后調用保存的接口:
~~~
var testDate = new Date('2016-06-04');
var testAVObject = new AV.Object('TestClass');
testAVObject.set('testDate', testDate);
testAVObject.save();
~~~
JavaScript SDK 在真正調用保存接口之前,會自動的調用一次序列化的方法,將?`Date`?類型的數據,轉化為如下格式的數據:
~~~
{
"__type": "Date",
"iso": "2015-11-21T18:02:52.249Z"
}
~~~
然后發送給云端,云端會自動進行反序列化,這樣自然就知道這個數據類型是日期,然后按照傳過來的有效值進行存儲。因此,開發者在進階開發的階段,最好是能掌握 LeanStorage 的數據協議。如下表介紹的就是一些默認的數據類型被序列化之后的格式:
| 類型 | 序列化之后的格式 |
| --- | --- |
| `Date` | `{"__type": "Date","iso": "2015-11-21T18:02:52.249Z"}` |
| `Buffer` | `{"__type": "Bytes","base64":"utf-8-encoded-string}"` |
| `Pointer` | `{"__type":"Pointer","className":"Todo","objectId":"55a39634e4b0ed48f0c1845c"}` |
| `AV.Relation` | `{"__type": "Relation","className": "Todo"}` |
## [文件](#文件)
文件存儲也是數據存儲的一種方式,圖像、音頻、視頻、通用文件等等都是數據的載體。很多開發者也習慣把復雜對象序列化之后保存成文件,比如 JSON 或 XML 文件。文件存儲在 LeanStorage 中被單獨封裝成一個?`AV.File`?來實現文件的上傳、下載等操作。
### [文件上傳](#文件上傳)
文件上傳是指開發者調用接口將文件存儲在云端,并且返回文件最終的 URL 的操作。文件上傳成功后會在系統表?`_File`?中生成一條記錄,此后該記錄無法被再次修改,包括?[metaData 字段](#文件元數據)?中的數據。如果?`_File`?表打開了?[刪除權限](#刪除),該記錄才可以被刪除。
#### [從數據流構建文件](#從數據流構建文件)
`AV.File`?支持圖片、視頻、音樂等常見的文件類型,以及其他任何二進制數據,在構建的時候,傳入對應的數據流即可:
~~~
var data = { base64: '6K+077yM5L2g5Li65LuA5LmI6KaB56C06Kej5oiR77yf' };
var file = new AV.File('resume.txt', data);
file.save();
var bytes = [0xBE, 0xEF, 0xCA, 0xFE];
var byteArrayFile = new AV.File('myfile.txt', bytes);
byteArrayFile.save();
~~~
上例將文件命名為?`resume.txt`,這里需要注意兩點:
* 不必擔心文件名沖突。每一個上傳的文件都有惟一的 ID,所以即使上傳多個文件名為?`resume.txt`?的文件也不會有問題。
* 給文件添加擴展名非常重要。云端通過擴展名來判斷文件類型,以便正確處理文件。所以要將一張 PNG 圖片存到?`AV.File`?中,要確保使用?`.png`?擴展名。
#### [從本地路徑構建文件](#從本地路徑構建文件)
大多數的客戶端應用程序都會跟本地文件系統產生交互,常用的操作就是讀取本地文件,如下代碼可以實現使用本地文件路徑構建一個?`AV.File`:
假設在頁面上有如下文件選擇框:
~~~
<input type="file" id="photoFileUpload"/>
~~~
上傳文件對應的代碼如下:
~~~
var fileUploadControl = $('#photoFileUpload')[0];
if (fileUploadControl.files.length > 0) {
var localFile = fileUploadControl.files[0];
var name = 'avatar.jpg';
var file = new AV.File(name, localFile);
file.save().then(function(file) {
// 文件保存成功
console.log(file.url());
}, function(error) {
// 異常處理
console.error(error);
});
}
~~~
#### [從網絡路徑構建文件](#從網絡路徑構建文件)
從一個已知的 URL 構建文件也是很多應用的需求。例如,從網頁上拷貝了一個圖像的鏈接,代碼如下:
~~~
var file = AV.File.withURL('Satomi_Ishihara.gif', 'http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif');
file.save().then(function(file) {
// 文件保存成功
console.log(file.url());
}, function(error) {
// 異常處理
console.error(error);
});
~~~
我們需要做出說明的是,[從本地路徑構建文件](#從本地路徑構建文件)?會產生實際上傳的流量,并且文件最后是存在云端,而?[從網絡路徑構建文件](#從網絡路徑構建文件)?的文件實體并不存儲在云端,只是會把文件的物理地址作為一個字符串保存在云端。
1
如果希望在云引擎環境里上傳文件,請參考我們的[網站托管開發指南](https://leancloud.cn/docs/leanengine_webhosting_guide-node.html#文件上傳)。
#### [上傳進度監聽](#上傳進度監聽)
一般來說,上傳文件都會有一個上傳進度條顯示用以提高用戶體驗:
~~~
file.save({
onprogress:function (e) {
console.log(e)
// { loaded: 1234, total: 2468, percent: 50 }
},
}).then(/* ... */);
// 2.0 之前版本的 SDK 中,save 的第二個參數 callbacks 不能省略:
file.save({
onprogress: function(e) { console.log(e); }
}, {}).then(/* ... */);
~~~
### [圖像縮略圖](#圖像縮略圖)
保存圖像時,如果想在下載原圖之前先得到縮略圖,方法如下:
~~~
//獲得寬度為100像素,高度200像素的縮略圖
var url = file.thumbnailURL(100, 200);
~~~
圖片最大不超過?20 MB?才可以獲取縮略圖。
### [文件元數據](#文件元數據)
`AV.File`?的?`metaData`?屬性,可以用來保存和獲取該文件對象的元數據信息。metaData 一旦保存到云端就無法再次修改。
~~~
// 獲取文件大小
var size = file.size();
// 上傳者(AV.User) 的 objectId,如果未登錄,默認為空
var ownerId = file.ownerId();
// 獲取文件的全部元信息
var metadata = file.metaData();
// 設置文件的作者
file.metaData('author', 'LeanCloud');
// 獲取文件的格式
var format = file.metaData('format');
~~~
### [文件查詢](#文件查詢)
文件的查詢依賴于文件在系統中的關系模型,例如,用戶的頭像,有一些用戶習慣直接在?`_User`?表中直接使用一個?`avatar`?列,然后里面存放著一個 url 指向一個文件的地址,但是,我們更推薦用戶使用 Pointer 來關聯一個 AV.User 和 AV.File,代碼如下:
~~~
var data = { base64: '文件的 base64 編碼' };
var avatar = new AV.File('avatar.png', data);
var user = new AV.User();
var randomUsername = 'Tom';
user.setUsername(randomUsername)
user.setPassword('leancloud');
user.set('avatar',avatar);
user.signUp().then(function (u){
});
~~~
### [刪除](#刪除)
當文件較多時,要把一些不需要的文件從云端刪除:
默認情況下,文件的刪除權限是關閉的,需要進入?[控制臺 > 存儲 >?`_File`](https://leancloud.cn/data.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/_File),選擇菜單?其他?>?權限設置?>?delete?來開啟。
~~~
var file = AV.File.createWithoutData('552e0a27e4b0643b709e891e');
file.destroy().then(function (success) {
}, function (error) {
});
~~~
## [查詢](#查詢)
`AV.Query`?是構建針對?`AV.Object`?查詢的基礎類。每次查詢默認最多返回 100 條符合條件的結果,要更改這一數值,請參考?[限定結果返回數量](#限定返回數量)。
### [示例數據結構](#示例數據結構)
熟悉本文所使用的相關數據表結構將有助于更好地理解后面的內容。

#### [Todo(待辦事項)](#Todo_待辦事項_)
| 字段 | 類型 | 說明 |
| --- | --- | --- |
| `content` | String | 事項的詳細內容 |
| `images` | AVFile | 與事項相關的圖片 |
| `location` | String | 處理該事項的地點 |
| `priority` | Number | 0 優先級最高,最迫切需要完成。 |
| `reminders` | Array | 設置提醒日期和時間 |
| `status` | Number | 0 未完成,1 已完成 |
| `title` | String | 事項的標題(簡短描述) |
| `views` | Number | 該事項被瀏覽過的次數 |
| `whereCreated` | AVGeoPoint | 該事項被創建時的地理定位 |
#### [TodoFolder(待辦事項的分組)](#TodoFolder_待辦事項的分組_)
| 字段 | 類型 | 說明 |
| --- | --- | --- |
| `containedTodos` | Relation | 所包含的 Todo,與表?`Todo`?相關聯。 |
| `name` | String | 分組的名稱,如家庭、會議。 |
| `owner` | Pointer | 分組的所有者或創建人,指向表?`_User` |
| `priority` | Number | 該分組的優先級別,0 優先級最高。 |
| `tags` | Relation | 標簽,與表?`Tag`?相關聯。 |
#### [Comment(待辦事項分組的評論)](#Comment_待辦事項分組的評論_)
| 字段 | 類型 | 說明 |
| --- | --- | --- |
| `content` | String | 評論的內容 |
| `likes` | Number | 點了贊就是 1,點了不喜歡為 -1,沒有做任何操作就為 0(默認)。 |
| `targetTodoFolder` | Pointer | 相關聯的待辦事項分組,指向表?`TodoFolder`?的 objectId |
#### [Tag(待辦事項分組的標簽)](#Tag_待辦事項分組的標簽_)
| 字段 | 類型 | 說明 |
| --- | --- | --- |
| `name` | String | 標簽的名稱,如今日必做、老婆吩咐、十分重要等。 |
| `targetTodoFolder` | Pointer | 相關聯的待辦事項分組,指向表?`TodoFolder`?的 objectId |
### [創建查詢實例](#創建查詢實例)
~~~
var query = new AV.Query('Todo');
~~~
最基礎的用法是根據 objectId 來查詢對象:
~~~
var query = new AV.Query('Todo');
query.get('57328ca079bc44005c2472d0').then(function (todo) {
// 成功獲得實例
// data 就是 id 為 57328ca079bc44005c2472d0 的 Todo 對象實例
}, function (error) {
// 異常處理
});
~~~
### [比較查詢](#比較查詢)
| 邏輯操作 | AVQuery 方法 |
| --- | --- |
| 等于 | `equalTo` |
| 不等于 | `notEqualTo` |
| 大于 | `greaterThan` |
| 大于等于 | `greaterThanOrEqualTo` |
| 小于 | `lessThan` |
| 小于等于 | `lessThanOrEqualTo` |
利用上述表格介紹的邏輯操作的接口,我們可以很快地構建條件查詢。
例如,查詢優先級小于 2 的所有 Todo :
~~~
var query = new AV.Query('Todo');
query.lessThan('priority', 2);
~~~
每次查詢默認最多返回 100 條符合條件的結果,要更改這一數值,請參考?[限定結果返回數量](#限定返回數量)。
以上邏輯用 SQL 語句表達為?`select * from Todo where priority < 2`。LeanStorage 也支持使用這種傳統的 SQL 語句查詢。具體使用方法請移步至?[Cloud Query Language(CQL)查詢](#CQL_查詢)。
查詢優先級大于等于 2 的 Todo:
~~~
query.greaterThanOrEqualTo('priority',2);
~~~
比較查詢只適用于可比較大小的數據類型,如整型、浮點等。
#### [多個查詢條件](#多個查詢條件)
當多個查詢條件并存時,它們之間默認為 AND 關系,即查詢只返回滿足了全部條件的結果。建立 OR 關系則需要使用?[組合查詢](#組合查詢)。
在簡單查詢中,如果對一個對象的同一屬性設置多個條件,那么先前的條件會被覆蓋,查詢只返回滿足最后一個條件的結果。例如要找出優先級為 0 和 1 的所有 Todo,錯誤寫法是:
~~~
var query = new AV.Query('Todo');
query.equalTo('priority', 0);
query.equalTo('priority', 1);
query.find().then(function (results) {
// 如果這樣寫,第二個條件將覆蓋第一個條件,查詢只會返回 priority = 1 的結果
}, function (error) {
});
~~~
正確作法是使用?[組合查詢 · OR 關系](#OR_查詢)?來構建這種條件。
### [字符串查詢](#字符串查詢)
前綴查詢類似于 SQL 的?`LIKE 'keyword%'`?條件。因為支持索引,所以該操作對于大數據集也很高效。
~~~
// 找出開頭是「早餐」的 Todo
var query = new AV.Query('Todo');
query.startsWith('content', '早餐');
// 找出包含 「bug」 的 Todo
var query = new AV.Query('Todo');
query.contains('content', 'bug');
~~~
包含查詢類似于 SQL 的?`LIKE '%keyword%'`?條件,比如查詢標題包含「李總」的 Todo:
~~~
query.contains('title','李總');
~~~
不包含查詢可以使用正則匹配查詢的方式來實現。例如,查詢標題不包含「機票」的 Todo:
~~~
var query = new AV.Query('Todo');
var regExp = new RegExp('^((?!機票).)*$', 'i');
query.matches('title', regExp);
~~~
正則匹配查詢只適用于字符串類型的數據。
但是基于正則的模糊查詢有兩個缺點:
* 當數據量逐步增大后,查詢效率將越來越低。
* 沒有文本相關性排序
因此我們推薦使用?[應用內搜索](#應用內搜索)?功能。它基于搜索引擎技術構建,提供更強大的搜索功能。
### [數組查詢](#數組查詢)
當一個對象有一個屬性是數組的時候,針對數組的元數據查詢可以有多種方式。例如,在?[數組](#更新數組)?一節中我們為 Todo 設置了 reminders 屬性,它就是一個日期數組,現在我們需要查詢所有在 8:30 會響起鬧鐘的 Todo 對象:
~~~
var query = new AV.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 AV.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 響起鬧鐘,它仍然是會被查詢出來的。
查詢「全不包含」的情況:
~~~
query.notContainedIn('reminders', reminderFilter);
~~~
### [空值查詢](#空值查詢)
假設用戶可以有選擇地為 Todo 上傳圖片來做標注,要想找出那些已有圖片的 Todo:
~~~
var aTodoAttachmentImage = AV.File.withURL('attachment.jpg', 'http://www.zgjm.org/uploads/allimg/150812/1_150812103912_1.jpg');
var todo = new AV.Object('Todo');
todo.set('images', aTodoAttachmentImage);
todo.set('content', '記得買過年回家的火車票!!!');
todo.save();
var query = new AV.Query('Todo');
query.exists('images');
query.find().then(function (results) {
// results 返回的就是有圖片的 Todo 集合
}, function (error) {
});
// 使用空值查詢獲取沒有圖片的 Todo
query.doesNotExist('images');
~~~
### [關系查詢](#關系查詢)
關聯數據查詢也可以通俗地理解為關系查詢,關系查詢在傳統型數據庫的使用中是很常見的需求,因此我們也提供了相關的接口來滿足開發者針對關聯數據的查詢。
首先,我們需要明確關系的存儲方式,再來確定對應的查詢方式。
#### [Pointer 查詢](#Pointer_查詢)
基于在?[Pointer](#Pointer)?小節介紹的存儲方式:每一個 Comment 都會有一個 TodoFolder 與之對應,用以表示 Comment 屬于哪個 TodoFolder。現在我已知一個 TodoFolder,想查詢所有的 Comnent 對象,可以使用如下代碼:
~~~
var query = new AV.Query('Comment');
var todoFolder = AV.Object.createWithoutData('TodoFolder', '5735aae7c4c9710060fbe8b0');
query.equalTo('targetTodoFolder', todoFolder);
// 想在查詢的同時獲取關聯對象的屬性則一定要使用 `include` 接口用來指定返回的 `key`
query.include('targetTodoFolder');
~~~
#### [`AV.Relation`?查詢](#AV_Relation_查詢)
假如用戶可以給 TodoFolder 增加一個 Tag 選項,用以表示它的標簽,而為了以后拓展 Tag 的屬性,就新建了一個 Tag 對象,如下代碼是創建 Tag 對象:
2
~~~
var tag = new AV.Object('Todo');
tag.set('name', '今日必做');
tag.save();
~~~
而 Tag 的意義在于一個 TodoFolder 可以擁有多個 Tag,比如「家庭」(TodoFolder) 擁有的 Tag 可以是:今日必做,老婆吩咐,十分重要。實現創建「家庭」這個 TodoFolder 的代碼如下:
~~~
var tag1 = new AV.Object('Todo');
tag1.set('name', '今日必做');
var tag2 = new AV.Object('Todo');
tag2.set('name', '老婆吩咐');
var tag3 = new AV.Object('Todo');
tag3.set('name', '十分重要');
var tags = [tag1, tag2, tag3];
AV.Object.saveAll(tags).then(function (savedTags) {
var todoFolder = new AV.Object('TodoFolder');
todoFolder.set('name', '家庭');
todoFolder.set('priority', 1);
var relation = todoFolder.relation('tags');
relation.add(tag1);
relation.add(tag2);
relation.add(tag3);
todoFolder.save();
}, function (error) {
});
~~~
查詢一個 TodoFolder 的所有 Tag 的方式如下:
~~~
var todoFolder = AV.Object.createWithoutData('Todo', '5735aae7c4c9710060fbe8b0');
var relation = todoFolder.relation('tags');
var query = relation.query();
query.find().then(function (results) {
// results 是一個 AV.Object 的數組,它包含所有當前 todoFolder 的 tags
}, function (error) {
});
~~~
反過來,現在已知一個 Tag,要查詢有多少個 TodoFolder 是擁有這個 Tag 的,可以使用如下代碼查詢:
~~~
var targetTag = AV.Object.createWithoutData('Tag', '5655729900b0bf3785ca8192');
var query = new AV.Query('TodoFolder');
query.equalTo('tags', targetTag);
query.find().then(function (results) {
// results 是一個 AV.Object 的數組
// results 指的就是所有包含當前 tag 的 TodoFolder
}, function (error) {
});
~~~
關于關聯數據的建模是一個復雜的過程,很多開發者因為在存儲方式上的選擇失誤導致最后構建查詢的時候難以下手,不但客戶端代碼冗余復雜,而且查詢效率低,為了解決這個問題,我們專門針對關聯數據的建模推出了一個詳細的文檔予以介紹,詳情請閱讀?[JavaScript 數據模型設計指南](https://leancloud.cn/docs/relation_guide-js.html)。
#### [關聯屬性查詢](#關聯屬性查詢)
正如在?[Pointer](#Pointer)?中保存 Comment 的 targetTodoFolder 屬性一樣,假如查詢到了一些 Comment 對象,想要一并查詢出每一條 Comment 對應的 TodoFolder 對象的時候,可以加上 include 關鍵字查詢條件。同理,假如 TodoFolder 表里還有 pointer 型字段 targetAVUser 時,再加上一個遞進的查詢條件,形如 include(b.c),即可一并查詢出每一條 TodoFolder 對應的 AVUser 對象。代碼如下:
~~~
var commentQuery = new AV.Query('Comment');
commentQuery.descending('createdAt');
commentQuery.limit(10);
commentQuery.include('targetTodoFolder');// 關鍵代碼,用 include 告知服務端需要返回的關聯屬性對應的對象的詳細信息,而不僅僅是 objectId
commentQuery.include('targetTodoFolder.targetAVUser');// 關鍵代碼,同上,會返回 targetAVUser 對應的對象的詳細信息,而不僅僅是 objectId
commentQuery.find().then(function (comments) {
// comments 是最近的十條評論, 其 targetTodoFolder 字段也有相應數據
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
// 并不需要網絡訪問
var todoFolder = comment.get('targetTodoFolder');
var avUser = todoFolder.get('targetAVUser');
}
}, function (error) {
});
~~~
#### [內嵌查詢](#內嵌查詢)
查詢點贊超過 20 次的 TodoFolder 的 Comment 評論(注意查詢針對的是?[Comment](#Comment_待辦事項分組的評論_)),使用內嵌查詢接口就可以通過一次查詢來達到目的。
~~~
// 構建內嵌查詢
var innerQuery = new AV.Query('TodoFolder');
innerQuery.greaterThan('likes', 20);
// 將內嵌查詢賦予目標查詢
var query = new AV.Query('Comment');
// 執行內嵌操作
query.matchesQuery('targetTodoFolder', innerQuery);
query.find().then(function (results) {
// results 就是符合超過 20 個贊的 TodoFolder 這一條件的 Comment 對象集合
}, function (error) {
});
query.doesNotMatchQuery('targetTodoFolder', innerQuery);
// 如此做將查詢出 likes 小于或者等于 20 的 TodoFolder 的 Comment 對象
~~~
與普通查詢一樣,內嵌查詢默認也最多返回 100 條記錄,想修改這一默認請參考?[限定結果返回數量](#限定返回數量)。如果所有返回的記錄沒有匹配到外層的查詢條件,那么整個查詢也查不到結果。
### [地理位置查詢](#地理位置查詢)
地理位置查詢是較為特殊的查詢,一般來說,常用的業務場景是查詢距離 xx 米之內的某個位置或者是某個建筑物,甚至是以手機為圓心,查找方圓多少范圍內餐廳等等。LeanStorage 提供了一系列的方法來實現針對地理位置的查詢。
#### [查詢位置附近的對象](#查詢位置附近的對象)
Todo 的?`whereCreated`(創建 Todo 時的位置)是一個?`AV.GeoPoint`?對象,現在已知了一個地理位置,現在要查詢?`whereCreated`?靠近這個位置的 Todo 對象可以使用如下代碼:
~~~
var query = new AV.Query('Todo');
var point = new AV.GeoPoint('39.9', '116.4');
query.withinKilometers('whereCreated', point, 2.0);
query.find().then(function (results) {
var nearbyTodos = results;
}, function (error) {
});
~~~
在上面的代碼中,`nearbyTodos`?返回的是與?`point`?這一點按距離排序(由近到遠)的對象數組。注意:如果在此之后又使用了?`ascending`?或?`descending`?方法,則按距離排序會被新排序覆蓋。
#### [查詢指定范圍內的對象](#查詢指定范圍內的對象)
要查找指定距離范圍內的數據,可使用?`whereWithinKilometers`?、?`whereWithinMiles`?或?`whereWithinRadians`?方法。 例如,我要查詢距離指定位置,2 千米范圍內的 Todo:
~~~
var query = new AV.Query('Todo');
var point = new AV.GeoPoint('39.9', '116.4');
query.withinKilometers('whereCreated', point, 2.0);
~~~
#### [注意事項](#注意事項)
使用地理位置需要注意以下方面:
* 每個?`AV.Object`?數據對象中只能有一個?`AV.GeoPoint`?對象的屬性。
* 地理位置的點不能超過規定的范圍。緯度的范圍應該是在?`-90.0`?到?`90.0`?之間,經度的范圍應該是在?`-180.0`?到?`180.0`?之間。如果添加的經緯度超出了以上范圍,將導致程序錯誤。
### [組合查詢](#組合查詢)
組合查詢就是把諸多查詢條件合并成一個查詢,再交給 SDK 去云端查詢。方式有兩種:OR 和 AND。
#### [OR 查詢](#OR_查詢)
OR 操作表示多個查詢條件符合其中任意一個即可。 例如,查詢優先級是大于等于 3 或者已經完成了的 Todo:
~~~
var priorityQuery = new AV.Query('Todo');
priorityQuery.greaterThanOrEqualTo('priority', 3);
var statusQuery = new AV.Query('Todo');
statusQuery.equalTo('status', 1);
var query = AV.Query.or(priorityQuery, statusQuery);
// 返回 priority 大于等于 3 或 status 等于 1 的 Todo
~~~
注意:OR 查詢中,子查詢中不能包含地理位置相關的查詢。
#### [AND 查詢](#AND_查詢)
AND 操作將滿足了所有查詢條件的對象返回給客戶端。例如,找到創建于?`2016-11-13`?至?`2016-12-02`?之間的 Todo:
~~~
var startDateQuery = new AV.Query('Todo');
startDateQuery.greaterThanOrEqualTo('createdAt', new Date('2016-11-13 00:00:00'));
var endDateQuery = new AV.Query('Todo');
endDateQuery.lessThan('createdAt', new Date('2016-12-03 00:00:00'));
var query = AV.Query.and(startDateQuery, endDateQuery);
~~~
可以對新創建的?`AV.Query`?添加額外的約束,多個約束將以 AND 運算符來聯接。
### [查詢結果數量和排序](#查詢結果數量和排序)
#### [獲取第一條結果](#獲取第一條結果)
例如很多應用場景下,只要獲取滿足條件的一個結果即可,例如獲取滿足條件的第一條 Todo:
~~~
var query = new AV.Query('Comment');
query.equalTo('priority', 0);
query.first().then(function (data) {
// data 就是符合條件的第一個 AV.Object
}, function (error) {
});
~~~
#### [限定返回數量](#限定返回數量)
為了防止查詢出來的結果過大,云端默認針對查詢結果有一個數量限制,即?`limit`,它的默認值是 100。比如一個查詢會得到 10000 個對象,那么一次查詢只會返回符合條件的 100 個結果。`limit`?允許取值范圍是 1 ~ 1000。例如設置返回 10 條結果:
~~~
var query = new AV.Query('Todo');
var now = new Date();
query.lessThanOrEqualTo('createdAt', now);//查詢今天之前創建的 Todo
query.limit(10);// 最多返回 10 條結果
~~~
#### [跳過數量](#跳過數量)
設置 skip 這個參數可以告知云端本次查詢要跳過多少個結果。將 skip 與 limit 搭配使用可以實現翻頁效果,這在客戶端做列表展現時,特別是在數據量龐大的情況下適合使用。例如,在翻頁中每頁顯示數量為 10,要獲取第 3 頁的對象:
~~~
var query = new AV.Query('Todo');
var now = new Date();
query.lessThanOrEqualTo('createdAt', now);//查詢今天之前創建的 Todo
query.limit(10);// 最多返回 10 條結果
query.skip(20);// 跳過 20 條結果
~~~
上述方法的執行效率比較低,因此不建議廣泛使用。建議選用?`createdAt`?或者?`updatedAt`?這類的時間戳進行分段查詢([示例](#AND_查詢))。
#### [屬性限定](#屬性限定)
通常列表展現的時候并不是需要展現某一個對象的所有屬性,例如,Todo 這個對象列表展現的時候,我們一般展現的是 title 以及 content,我們在設置查詢的時候,也可以告知云端需要返回的屬性有哪些,這樣既滿足需求又節省了流量,也可以提高一部分的性能,代碼如下:
~~~
var query = new AV.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
}, function (error) {
// 異常處理
});
~~~
屬性限定也支持 Pointer 類型的字段。例如,如果還想獲取 Todo 這個對象的所有者信息(owner 屬性,Pointer 類型),僅展示這個所有者的 username,可以這樣限定屬性:
~~~
query.select('owner.username');
~~~
#### [統計總數量](#統計總數量)
通常用戶在執行完搜索后,結果頁面總會顯示出諸如「搜索到符合條件的結果有 1020 條」這樣的信息。例如,查詢一下今天一共完成了多少條 Todo:
2
~~~
var query = new AV.Query('Todo');
query.equalTo('status', 1);
query.count().then(function (count) {
console.log(count);
}, function (error) {
});
~~~
#### [排序](#排序)
對于數字、字符串、日期類型的數據,可對其進行升序或降序排列。
~~~
// 按時間,升序排列
query.ascending('createdAt');
// 按時間,降序排列
query.descending('createdAt');
~~~
一個查詢可以附加多個排序條件,如按 priority 升序、createdAt 降序排列:
~~~
var query = new AV.Query('Todo');
query.addAscending('priority');
query.addDescending('createdAt');
~~~
### [CQL 查詢](#CQL_查詢)
Cloud Query Language(CQL)是 LeanStorage 獨創的使用類似 SQL 語法來實現云端查詢功能的語言,具有 SQL 開發經驗的開發者可以方便地使用此接口實現查詢。
分別找出 status = 1 的全部 Todo 結果,以及 priority = 0 的 Todo 的總數:
~~~
// 新建 AVUser 對象實例
AV.Query.doCloudQuery(cql).then(function (data) {
// results 即為查詢結果,它是一個 AV.Object 數組
var results = data.results;
}, function (error) {
});
cql = 'select * from %@ where status = 1';
AV.Query.doCloudQuery(cql).then(function (data) {
// 獲取符合查詢的數量
var count = data.count;
}, function (error) {
});
~~~
通常查詢語句會使用變量參數,為此我們提供了與 Java JDBC 所使用的 PreparedStatement 占位符查詢相類似的語法結構。
查詢 status = 0、priority = 1 的 Todo:
~~~
// 帶有占位符的 cql 語句
var cql = 'select * from Todo where status = ? and priority = ?';
var pvalues = [0, 1];
AV.Query.doCloudQuery(cql, pvalues).then(function (data) {
// results 即為查詢結果,它是一個 AV.Object 數組
var results = data.results;
}, function (error) {
});
~~~
目前 CQL 已經支持數據的更新 update、插入 insert、刪除 delete 等 SQL 語法,更多內容請參考?[Cloud Query Language 詳細指南](https://leancloud.cn/docs/cql_guide.html)。
### [查詢性能優化](#查詢性能優化)
影響查詢性能的因素很多。特別是當查詢結果的數量超過 10 萬,查詢性能可能會顯著下降或出現瓶頸。以下列舉一些容易降低性能的查詢方式,開發者可以據此進行有針對性的調整和優化,或盡量避免使用。
* 不等于和不包含查詢(無法使用索引)
* 通配符在前面的字符串查詢(無法使用索引)
* 有條件的 count(需要掃描所有數據)
* skip 跳過較多的行數(相當于需要先查出被跳過的那些行)
* 無索引的排序(另外除非復合索引同時覆蓋了查詢和排序,否則只有其中一個能使用索引)
* 無索引的查詢(另外除非復合索引同時覆蓋了所有條件,否則未覆蓋到的條件無法使用索引,如果未覆蓋的條件區分度較低將會掃描較多的數據)
## [Promise](#Promise)
每一個在 LeanCloud JavaScript SDK 中的異步方法都會返回一個?`Promise`,可以通過這個 promise 來處理該異步方法的完成與異常。
~~~
// 這是一個比較完整的例子,具體方法可以看下面的文檔
// 查詢某個 AV.Object 實例,之后進行修改
var query = new AV.Query('TestObject');
query.equalTo('name', 'hjiang');
// find 方法是一個異步方法,會返回一個 Promise,之后可以使用 then 方法
query.find().then(function(results) {
// 返回一個符合條件的 list
var obj = results[0];
obj.set('phone', '182xxxx5548');
// save 方法也是一個異步方法,會返回一個 Promise,所以在此處,你可以直接 return 出去,后續操作就可以支持鏈式 Promise 調用
return obj.save();
}).then(function() {
// 這里是 save 方法返回的 Promise
console.log('設置手機號碼成功');
}).catch(function(error) {
// catch 方法寫在 Promise 鏈式的最后,可以捕捉到全部 error
console.error(error);
});
~~~
### [then 方法](#then_方法)
每一個 Promise 都有一個叫?`then`?的方法,這個方法接受一對 callback。第一個 callback 在 promise 被解決(`resolved`,也就是正常運行)的時候調用,第二個會在 promise 被拒絕(`rejected`,也就是遇到錯誤)的時候調用。
~~~
obj.save().then(function(obj) {
//對象保存成功
}, function(error) {
//對象保存失敗,處理 error
});
~~~
其中第二個參數是可選的。
你還可以使用?`catch`?三個方法,將邏輯寫成:
~~~
obj.save().then(function(obj) {
//對象保存成功
}).catch(function(error) {
//對象保存失敗,處理 error
});
~~~
### [將 Promise 組織在一起](#將_Promise_組織在一起)
Promise 比較神奇,可以代替多層嵌套方式來解決發送異步請求代碼的調用順序問題。如果一個 Promise 的回調會返回一個 Promise,那么第二個 then 里的 callback 在第一個 then 的 callback 沒有解決前是不會解決的,也就是所謂?Promise Chain。
~~~
// 將內容按章節順序添加到頁面上
var chapterIds = [
'584e1c408e450a006c676162', // 第一章
'584e1c43128fe10058b01cf5', // 第二章
'581aff915bbb500059ca8d0b' // 第三章
];
new AV.Query('Chapter').get(chapterIds[0]).then(function(chapter0) {
// 向頁面添加內容
addHtmlToPage(chapter0.get('content'));
// 返回新的 Promise
return new AV.Query('Chapter').get(chapterIds[1]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.get('content'));
return new AV.Query('Chapter').get(chapterIds[2]);
}).then(function(chapter2) {
addHtmlToPage(chapter2.get('content'));
// 完成
});
~~~
### [錯誤處理](#錯誤處理)
如果任意一個在鏈中的 Promise 拋出一個異常的話,所有的成功的 callback 在接下 來都會被跳過直到遇到一個處理錯誤的 callback。
通常來說,在正常情況的回調函數鏈的末尾,加一個錯誤處理的回調函數,是一種很 常見的做法。
利用?`try,catch`?方法可以將上述代碼改寫為:
~~~
new AV.Query('Chapter').get(chapterIds[0]).then(function(chapter0) {
addHtmlToPage(chapter0.get('content'));
// 強制失敗
throw new Error('出錯啦');
return new AV.Query('Chapter').get(chapterIds[1]);
}).then(function(chapter1) {
// 這里的代碼將被忽略
addHtmlToPage(chapter1.get('content'));
return new AV.Query('Chapter').get(chapterIds[2]);
}).then(function(chapter2) {
// 這里的代碼將被忽略
addHtmlToPage(chapter2.get('content'));
}).catch(function(error) {
// 這個錯誤處理函數將被調用,錯誤信息是 '出錯啦'.
console.error(error.message);
});
~~~
### [JavaScript Promise 迷你書](#JavaScript_Promise_迷你書)
如果你想更深入地了解和學習 Promise,包括如何對并行的異步操作進行控制,我們推薦閱讀?[《JavaScript Promise迷你書(中文版)》](http://liubin.github.io/promises-book/)?這本書。
## [用戶](#用戶)
用戶系統幾乎是每款應用都要加入的功能。除了基本的注冊、登錄和密碼重置,移動端開發還會使用手機號一鍵登錄、短信驗證碼登錄等功能。LeanStorage 提供了一系列接口來幫助開發者快速實現各種場景下的需求。
`AV.User`?是用來描述一個用戶的特殊對象,與之相關的數據都保存在?`_User`?數據表中。
### [用戶的屬性](#用戶的屬性)
#### [默認屬性](#默認屬性-1)
用戶名、密碼、郵箱是默認提供的三個屬性,訪問方式如下:
~~~
AV.User.logIn('Tom', 'cat!@#123').then(function (loginedUser) {
console.log(loginedUser);
var username = loginedUser.getUsername();
var email = loginedUser.getEmail();
// 請注意,密碼不會明文存儲在云端,因此密碼只能重置,不能查看
}, function (error) {
});
~~~
請注意代碼中,密碼是僅僅是在注冊的時候可以設置的屬性(這部分代碼可參照?[用戶名和密碼注冊](#用戶名和密碼注冊)),它在注冊完成之后并不會保存在本地(SDK 不會以明文保存密碼這種敏感數據),所以在登錄之后,再訪問密碼這個字段是為空的。
#### [自定義屬性](#自定義屬性)
用戶對象和普通對象一樣也支持添加自定義屬性。例如,為當前用戶添加年齡屬性?`age`:
~~~
AV.User.logIn('Tom', 'cat!@#123').then(function (loginedUser) {
loginedUser.set('age', 25);
loginedUser.save();
}, function (error) {
// 異常處理
console.error(error);
});
~~~
#### [修改屬性](#修改屬性)
很多開發者會有這樣的疑問:「為什么我不能修改任意一個用戶的屬性?」
> 因為很多時候,就算是開發者也不要輕易修改用戶的基本信息,例如用戶的手機號、社交賬號等個人信息都比較敏感,應該由用戶在 App 中自行修改。所以為了保證用戶的數據僅在用戶自己已登錄的狀態下才能修改,云端對所有針對?`AV.User`?對象的數據操作都要做驗證。
>
>
>
> >
>
例如,先為當前用戶增加一個 age 屬性,保存后再更改它的值:
~~~
AV.User.logIn('Tom', 'cat!@#123').then(function (loginedUser) {
// 25
console.log(loginedUser.get('age'));
loginedUser.set('age', 18);
return loginedUser.save();
}).then(function(loginedUser) {
// 18
console.log(loginedUser.get('age'));
}).catch(function(error) {
// 異常處理
console.error(error);
});
~~~
`AV.User`?的自定義屬性在使用上與?`AV.Object`?沒有本質區別。
### [注冊](#注冊)
#### [手機號碼登錄](#手機號碼登錄)
一些應用為了提高首次使用的友好度,一般會允許用戶瀏覽一些內容,直到用戶發起了一些操作才會要求用戶輸入一個手機號,而云端會自動發送一條驗證碼的短信給用戶的手機號,最后驗證一下,完成一個用戶注冊并且登錄的操作,例如很多團購類應用都有這種用戶場景。
2
首先調用發送驗證碼的接口:
~~~
AV.Cloud.requestSmsCode('13577778888').then(function (success) {
}, function (error) {
});
~~~
然后在 UI 上給與用戶輸入驗證碼的輸入框,用戶點擊登錄的時候調用如下接口:
~~~
AV.User.signUpOrlogInWithMobilePhone('13577778888', '123456').then(function (success) {
// 成功
}, function (error) {
// 失敗
});
~~~
#### [用戶名和密碼注冊](#用戶名和密碼注冊)
采用「用戶名 + 密碼」注冊時需要注意:密碼是以明文方式通過 HTTPS 加密傳輸給云端,云端會以密文存儲密碼,并且我們的加密算法是無法通過所謂「彩虹表撞庫」獲取的,這一點請開發者放心。換言之,用戶的密碼只可能用戶本人知道,開發者不論是通過控制臺還是 API 都是無法獲取。另外我們需要強調在客戶端,應用切勿再次對密碼加密,這會導致重置密碼等功能失效。
3
例如,注冊一個用戶的示例代碼如下(用戶名?`Tom`?密碼?`cat!@#123`):
~~~
// 新建 AVUser 對象實例
var user = new AV.User();
// 設置用戶名
user.setUsername('Tom');
// 設置密碼
user.setPassword('cat!@#123');
// 設置郵箱
user.setEmail('tom@leancloud.cn');
user.signUp().then(function (loginedUser) {
console.log(loginedUser);
}, function (error) {
});
~~~
我們建議在可能的情況下盡量使用異步版本的方法,這樣就不會影響到應用程序主 UI 線程的響應。
如果注冊不成功,請檢查一下返回的錯誤對象。最有可能的情況是用戶名已經被另一個用戶注冊,錯誤代碼?[202](https://leancloud.cn/docs/error_code.html#_202),即?`_User`?表中的?`username`?字段已存在相同的值,此時需要提示用戶嘗試不同的用戶名來注冊。同樣,郵件?`email`?和手機號碼?`mobilePhoneNumber`?字段也要求在各自的列中不能有重復值出現,否則會出現?[203](https://leancloud.cn/docs/error_code.html#_203)、[214](https://leancloud.cn/docs/error_code.html#_214)?錯誤。
3
開發者也可以要求用戶使用 Email 做為用戶名注冊,即在用戶提交信息后將?`_User`?表中的?`username`?和?`email`?字段都設為相同的值,這樣做的好處是用戶在忘記密碼的情況下可以直接使用「[郵箱重置密碼](#重置密碼)」功能,無需再額外綁定電子郵件。
關于自定義郵件模板和驗證鏈接,請參考《[自定義應用內用戶重設密碼和郵箱驗證頁面](https://blog.leancloud.cn/607/)》。
#### [第三方賬號登錄](#第三方賬號登錄)
為了簡化用戶注冊的繁瑣流程,許多應用都在登錄界面提供了第三方社交賬號登錄的按鈕選項,例如微信、QQ、微博、Github、豆瓣、Twitter、FaceBook 等,以此來提高用戶體驗。LeanCloud 封裝的 AV.User 對象也支持通過第三方賬號的 accessToken 信息來創建一個用戶。例如,使用微信授權信息創建 AV.User 的代碼如下:
~~~
AV.User.signUpOrlogInWithAuthData({
// 微博(weibo)用 uid
// 微信(weixin)和 QQ(qq)用 openid
"openid": "oPrJ7uM5Y5oeypd0fyqQcKCaRv3o",
"access_token": "OezXcEiiBSKSxW0eoylIeNFI3H7HsmxM7dUj1dGRl2dXJOeIIwD4RTW7Iy2IfJePh6jj7OIs1GwzG1zPn7XY_xYdFYvISeusn4zfU06NiA1_yhzhjc408edspwRpuFSqtYk0rrfJAcZgGBWGRp7wmA",
"expires_at": "2016-01-06T11:43:11.904Z"
}, 'weixin').then(function (s) {
}, function (e) {
});
~~~
目前我們僅支持驗證以下平臺的?`access_token`?的合法性:
* 微信
* QQ
* 微博
要接入其他平臺,開發者需要完成以下步驟:
* 進入?控制臺?>?應用設置?>?應用選項?中取消?第三方登錄時,驗證用戶 AccessToken 合法性?的勾選。這樣開發者要自行驗證?`access_token`?的合法性。
* 確保 authData 包含?`uid`(即將上例代碼中的?`openid`?換為?`uid`),否則 SDK 會返回「無效的第三方注冊數據(authData)」的錯誤。
以使用 Github 登錄為例:
~~~
AV.User.signUpOrlogInWithAuthData({
'uid': githubClientId,
'access_token': accessToken
}, 'github');
~~~
更多用法請參考?[REST API · 連接用戶賬戶和第三方平臺](https://leancloud.cn/docs/rest_api.html#連接用戶賬戶和第三方平臺)。
#### [設置手機號碼](#設置手機號碼)
微信、陌陌等流行應用都會建議用戶將賬號和一個手機號綁定,這樣方便進行身份認證以及日后的密碼找回等安全模塊的使用。我們也提供了一整套發送短信驗證碼以及驗證手機號的流程,這部分流程以及代碼演示請參考?[JavaScript 短信服務使用指南](https://leancloud.cn/docs/sms_guide-js.html#注冊驗證)。
#### [驗證郵箱](#驗證郵箱)
許多應用會通過驗證郵箱來確認用戶注冊的真實性。如果在?[控制臺 > 應用設置 > 應用選項](https://leancloud.cn/app.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/permission)?中勾選了?用戶注冊時,發送驗證郵件,那么當一個?`AVUser`?在注冊時設置了郵箱,云端就會向該郵箱自動發送一封包含了激活鏈接的驗證郵件,用戶打開該郵件并點擊激活鏈接后便視為通過了驗證。有些用戶可能在注冊之后并沒有點擊激活鏈接,而在未來某一個時間又有驗證郵箱的需求,這時需要調用如下接口讓云端重新發送驗證郵件:
~~~
AV.User.requestEmailVerfiy('abc@xyz.com').then(function (result) {
console.log(JSON.stringify(result));
}, function (error) {
console.log(JSON.stringify(error));
});
~~~
### [登錄](#登錄)
我們提供了多種登錄方式,以滿足不同場景的應用。
#### [用戶名和密碼登錄](#用戶名和密碼登錄)
~~~
AV.User.logIn('Tom', 'cat!@#123').then(function (loginedUser) {
console.log(loginedUser);
}, function (error) {
});
~~~
#### [手機號和密碼登錄](#手機號和密碼登錄)
[JavaScript 短信服務使用指南](https://leancloud.cn/docs/sms_guide-js.html#注冊驗證)?可以幫助你更好地理解手機號匹配密碼登錄的流程以及適用范圍,所以推薦詳細閱讀。
~~~
AV.User.logInWithMobilePhone('13577778888', 'cat!@#123').then(function (loginedUser) {
console.log(loginedUser);
}, (function (error) {
}));
~~~
以上的手機號碼即使沒有經過驗證,只要密碼正確也可以成功登錄。如果希望阻止未驗證的手機號碼用于登錄,則需要在?[控制臺 > 應用設置 > 應用選項](https://leancloud.cn/app.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/permission)?中勾選?未驗證手機號碼的用戶,禁止登錄**。這種方式也提高了用戶賬號的合法性與安全性。
#### [手機號和驗證碼登錄](#手機號和驗證碼登錄)
首先,調用發送登錄驗證碼的接口:
~~~
AV.User.requestLoginSmsCode('13577778888').then(function (success) {
}, function (error) {
});
~~~
然后在界面上引導用戶輸入收到的 6 位短信驗證碼:
~~~
AV.User.logInWithMobilePhoneSmsCode('13577778888', '238825').then(function (success) {
}, function (error) {
});
~~~
#### [當前用戶](#當前用戶)
打開微博或者微信,它不會每次都要求用戶都登錄,這是因為它將用戶數據緩存在了客戶端。 同樣,只要是調用了登錄相關的接口,LeanCloud SDK 都會自動緩存登錄用戶的數據。 例如,判斷當前用戶是否為空,為空就跳轉到登錄頁面讓用戶登錄,如果不為空就跳轉到首頁:
~~~
var currentUser = AV.User.current();
if (currentUser) {
// 跳轉到首頁
}
else {
//currentUser 為空時,可打開用戶注冊界面…
}
~~~
如果不調用?[登出](#登出)?方法,當前用戶的緩存將永久保存在客戶端。
#### [SessionToken](#SessionToken)
所有登錄接口調用成功之后,云端會返回一個 SessionToken 給客戶端,客戶端在發送 HTTP 請求的時候,JavaScript SDK 會在 HTTP 請求的 Header 里面自動添加上當前用戶的 SessionToken 作為這次請求發起者?`AV.User`?的身份認證信息。
如果在?[控制臺 > 應用設置 > 應用選項](https://leancloud.cn/app.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/permission)?中勾選了?密碼修改后,強制客戶端重新登錄,那么當用戶密碼再次被修改后,已登錄的用戶對象就會失效,開發者需要使用更改后的密碼重新調用登錄接口,使 SessionToken 得到更新,否則后續操作會遇到?[403 (Forbidden)](https://leancloud.cn/docs/error_code.html#_403)?的錯誤。
##### 驗證 SessionToken 是否在有效期內
~~~
var currentUser = AV.User.current();
currentUser.isAuthenticated().then(function(authenticated){
// console.log(authenticated); 根據需求進行后續的操作
});
~~~
#### [賬戶鎖定](#賬戶鎖定)
輸入錯誤的密碼或驗證碼會導致用戶登錄失敗。如果在 15 分鐘內,同一個用戶登錄失敗的次數大于 6 次,該用戶賬戶即被云端暫時鎖定,此時云端會返回錯誤碼?`{"code":1,"error":"登錄失敗次數超過限制,請稍候再試,或者通過忘記密碼重設密碼。"}`,開發者可在客戶端進行必要提示。
鎖定將在最后一次錯誤登錄的 15 分鐘之后由云端自動解除,開發者無法通過 SDK 或 REST API 進行干預。在鎖定期間,即使用戶輸入了正確的驗證信息也不允許登錄。這個限制在 SDK 和云引擎中都有效。
### [重置密碼](#重置密碼)
#### [郵箱重置密碼](#郵箱重置密碼)
我們都知道,應用一旦加入賬戶密碼系統,那么肯定會有用戶忘記密碼的情況發生。對于這種情況,我們為用戶提供了一種安全重置密碼的方法。
重置密碼的過程很簡單,用戶只需要輸入注冊的電子郵件地址即可:
~~~
AV.User.requestPasswordReset('myemail@example.com').then(function (success) {
}, function (error) {
});
~~~
密碼重置流程如下:
1. 用戶輸入注冊的電子郵件,請求重置密碼;
2. LeanStorage 向該郵箱發送一封包含重置密碼的特殊鏈接的電子郵件;
3. 用戶點擊重置密碼鏈接后,一個特殊的頁面會打開,讓他們輸入新密碼;
4. 用戶的密碼已被重置為新輸入的密碼。
關于自定義郵件模板和驗證鏈接,請參考《[自定義應用內用戶重設密碼和郵箱驗證頁面](https://blog.leancloud.cn/607/)》。
#### [手機號碼重置密碼](#手機號碼重置密碼)
與使用?[郵箱重置密碼](#郵箱重置密碼)?類似,「手機號碼重置密碼」使用下面的方法來獲取短信驗證碼:
~~~
AV.User.requestPasswordResetBySmsCode('18612340000').then(function (success) {
}, function (error) {
});
~~~
注意!用戶需要先綁定手機號碼,然后使用短信驗證碼來重置密碼:
~~~
AV.User.resetPasswordBySmsCode('123456', 'thenewpassword').then(function (success) {
}, function (error) {
});
~~~
#### [登出](#登出)
用戶登出系統時,SDK 會自動清理緩存信息。
~~~
AV.User.logOut();
// 現在的 currentUser 是 null 了
var currentUser = AV.User.current();
~~~
### [用戶的查詢](#用戶的查詢)
為了安全起見,新創建的應用的?`_User`?表默認關閉了 find 權限,這樣每位用戶登錄后只能查詢到自己在?`_User`?表中的數據,無法查詢其他用戶的數據。如果需要讓其查詢其他用戶的數據,建議單獨創建一張表來保存這類數據,并開放這張表的 find 查詢權限。
設置數據表權限的方法,請參考?[數據與安全 · Class 級別的權限](https://leancloud.cn/docs/data_security.html#Class_級別的_ACL)。我們推薦開發者在?[云引擎](https://leancloud.cn/docs/leanengine_overview.html)?中封裝用戶查詢,只查詢特定條件的用戶,避免開放?`_User`?表的全部查詢權限。
查詢用戶代碼如下:
~~~
var query = new AV.Query('_User');
~~~
### [瀏覽器中查看用戶表](#瀏覽器中查看用戶表)
用戶表是一個特殊的表,專門存儲用戶對象。在瀏覽器端,你會看到一個?`_User`?表。
## [角色](#角色)
關于用戶與角色的關系,我們有一個更為詳盡的文檔介紹這部分的內容,并且針對權限管理有深入的講解,詳情請點擊?[JavaScript 權限管理使用指南](https://leancloud.cn/docs/acl_guide-js.html)。
## [應用內搜索](#應用內搜索)
應用內搜索是一個針對應用數據進行全局搜索的接口,它基于搜索引擎構建,提供更強大的搜索功能。要深入了解其用法和閱讀示例代碼,請閱讀?[JavaScript 應用內搜索指南](https://leancloud.cn/docs/app_search_guide.html)。
## [應用內社交](#應用內社交)
應用內社交,又稱「事件流」,在應用開發中出現的場景非常多,包括用戶間關注(好友)、朋友圈(時間線)、狀態、互動(點贊)、私信等常用功能,請參考?[JavaScript 應用內社交模塊](https://leancloud.cn/docs/status_system.html#JavaScript_SDK)。
## [Push 通知](#Push_通知)
通過 JavaScript SDK 也可以向移動設備推送消息。
一個簡單例子推送給所有訂閱了?`public`?頻道的設備:
~~~
AV.Push.send({
channels: [ 'public' ],
data: {
alert: 'public message'
}
});
~~~
這就向訂閱了?`public`?頻道的設備發送了一條內容為?`public message`?的消息。
如果希望按照某個?`_Installation`?表的查詢條件來推送,例如推送給某個?`installationId`?的 Android 設備,可以傳入一個?`AV.Query`?對象作為?`where`?條件:
~~~
var query = new AV.Query('_Installation');
query.equalTo('installationId', installationId);
AV.Push.send({
where: query,
data: {
alert: 'Public message'
}
});
~~~
此外,如果你覺得 AV.Query 太繁瑣,也可以寫一句?[CQL](https://leancloud.cn/docs/cql_guide.html)?來搞定:
~~~
AV.Push.send({
cql: 'select * from _Installation where installationId="設備id"',
data: {
alert: 'Public message'
}
});
~~~
`AV.Push`?的更多使用信息參考 API 文檔?[AV.Push](https://leancloud.github.io/javascript-sdk/docs/AV.Push.html)。更多推送的查詢條件和格式,請查閱?[消息推送指南](https://leancloud.cn/docs/push_guide.html)。
iOS 設備可以通過?`prod`?屬性指定使用測試環境還是生產環境證書:
~~~
AV.Push.send({
prod: 'dev',
data: {
alert: 'public message'
}
});
~~~
`dev`?表示開發證書,`prod`?表示生產證書,默認生產證書。
## [WebView 中使用](#WebView_中使用)
JS SDK 支持在各種 WebView 中使用(包括 PhoneGap/Cordova、微信 WebView 等)。
### [Android WebView 中使用](#Android_WebView_中使用)
如果是 Android WebView,在 Native 代碼創建 WebView 的時候你需要打開幾個選項, 這些選項生成 WebView 的時候默認并不會被打開,需要配置:
1. 因為我們 JS SDK 目前使用了 window.localStorage,所以你需要開啟 WebView 的 localStorage:
~~~
yourWebView.getSettings().setDomStorageEnabled(true);
~~~
2. 如果你希望直接調試手機中的 WebView,也同樣需要在生成 WebView 的時候設置遠程調試,具體使用方式請參考?[Google 官方文檔](https://developer.chrome.com/devtools/docs/remote-debugging)。
~~~
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
yourWebView.setWebContentsDebuggingEnabled(true);
}
~~~
注意:這種調試方式僅支持 Android 4.4 已上版本(含 4.4)
3. 如果你是通過 WebView 來開發界面,Native 調用本地特性的 Hybrid 方式開發你的 App。比較推薦的開發方式是:通過 Chrome 的開發者工具開發界面部分,當界面部分完成,與 Native 再來做數據連調,這種時候才需要用 Remote debugger 方式在手機上直接調試 WebView。這樣做會大大節省你開發調試的時間,不然如果界面都通過 Remote debugger 方式開發,可能效率較低。
4. 為了防止通過 JavaScript 反射調用 Java 代碼訪問 Android 文件系統的安全漏洞,在 Android 4.2 以后的系統中間,WebView 中間只能訪問通過?[@JavascriptInterface](http://developer.android.com/reference/android/webkit/JavascriptInterface.html)?標記過的方法。如果你的目標用戶覆蓋 4.2 以上的機型,請注意加上這個標記,以避免出現?Uncaught TypeError。