Fluent client api for LeanCloud services
简介
fluent-leancloud 用于帮助开发者构建基于 LeanCloud 服务的流畅API。LeanCloud.cn 的云服务提供了足够的后台服务帮助开发者专注于App本身的开发。但是官方客户端SDK的设计上和当下流行的开发方式上有一些不匹配,以至于使用起来有相当的不便。特别是存储部分,由于使用了Data Access Object的模式,使得Data Object维护了大量的内部状态,这个和我们目前项目中所提倡的中的Immutable Data有很大的违和感。 这个项目的目标是希望通过直接调用http restful endpoints实现数据的建模,ORM,用户管理,实时通讯等服务。构建一个简洁,可定制,轻量的开发工具集,特别针对使用React, Flux, Immutable的环境,从而为开发者提供一个官方SDK外的选择。
以下是非注解的使用示例,具体使用方法请往下阅读。
import {LeancloudHttp, LeancloudApi} from 'fluent-leancloud';
const {appId, appKey, masterKey} = process.env;
const http = LeancloudHttp({appId, appKey, masterKey});
const {factory} = LeancloudApi(http);
// Default resource model with customiseMethod
const Todo = factory({type: 'Todo'});
// Relation Model hasMany by pointer
const Post = factory({type: 'Post'}, (post)=>{
post.hasMany('comments', {type:'Comment', by:'pointer', foreignKey:'post'});
});
// Relation Model belongsTo
const Comment = factory({type: 'Comment'}, (comment)=>{
comment.belongsTo('post', {type:'Post'});
});
// Relation Model hasMany by relations
const Role = factory({type:'_Role'}, role=>{
role.hasMany('users', {type: '_User', by: 'relations'});
role.hasMany('roles', {type: '_Role', by: 'relations'});
})
// ---------------
// Using http
// ---------------
async function httpUsageExample(){
const todoId = "57e5c7........";
const appInfo = await http.get('/stats/appinfo');
const todo = await http.post('/classes/Todo', {content:"演示TODO", completed:false});
await http.put(`/classes/Todo/${todoId}`, {completed:true});
await http.delete(`/classes/Todo/${todoId}`);
}
// -----------------------------
// Using default resource model
// -----------------------------
async function defaultResourceModelExample(){
// collection methods
const newTodo = await Todo.create({content: "演示", completed:false});
const todoList = await Todo.find({where: {completed: false}, limit:2, order:'createdAt'});
const aTodo = await Todo.findOne({where: {completed: false}});
const todoCount = await Todo.count();
// instance methods
const objectId="5811e20.......";
const todo = await Todo(objectId).get();
const result = await Todo(objectId).update({completed: true});
await Todo(objectId).increase('viewCount', 2);
await Todo(objectId).destroy();
}
// -----------------------------
// Extend default resource model
// -----------------------------
async function extendDefaultModelExample(){
const MyTodo = factory({type: 'Todo'}, (todo)=>{
todo.instance({
addTags:{
verb: 'put',
args: ['labels'],
data: ({id, labels})=>({id, tags: Array.addUnique(labels)})
}
});
});
await MyTodo("5811e20.......").addTags(['work','programming']);
}
// -----------------------------
// hasMany by pointer
// belongsTo
// -----------------------------
async function hasManyBelongsToExample(){
// belongsTo relation methods
const postId = "581709156.........";
const aComment = await Post(postId).comments.create({content:"it is good"});
const comments = await Post(postId).comments.find({order:"createdAt"});
const commentCount= await Post(postId).comments.count();
// belongsTo relation methods
const commentId = "5817096........";
await Comment(commentId).post.set(postId);
await Comment(commentId).post.get();
}
// -----------------------------
// hasMany by relations
// -----------------------------
async function hasManyByRelationExamples(roleId, userId){
await Role(roleId).users.add(userId);
await Role(roleId).users.remove(userId);
await Role(roleId).users.find({where:{verified:false}});
await Role(roleId).users.count();
}
// -----------------------------
// seeding roles example
// -----------------------------
async function seedRolesExample(){
const {objectId: admin} = await Role.create({name:'admin', ACL:{"*":{read: true}}});
const {objectId: manager} = await Role.create({name:'manager', ACL:{"*":{read: true}}});
await Role(admin).roles.add(manager);
return {admin, manager}
}
使用场景
fluent-leancloud 并不假设开发者的使用环境,设计初衷是为了构建一个轻量级的工具集,这里从基本用途到ORM建模的路径简单介绍一下三种使用场景。
1. 作为一个简单的HTTP客户端使用
这是最基本的应用场景,fluent-leancloud 提供了一个基于fetch的HTTP client,这个客户端封装了LeanCloud的鉴权方法,让开发者直接使用http的(get | put | post | delete)方法访问Leancloud服务而不用关心请求头中 X-LC-Id, X-LC-Sign, X-LC-Session 构建方法。
//Node环境下必须提供一个fetch polyfil
import 'whatwg-fetch';
import {LeancloudHttp} from 'fluent-leancloud';
// 创建一个Http client
const http = LeancloudHttp({
appId: "应用appId",
appKey: '应用appKey',
masterKey: '应用masterKey,可选是可选项'
})
// 获取app信息
http.get('/stats/appinfo').then(console.log, console.error);
// 创建一条Todo记录
http.post('/classes/Todo', {content:"演示TODO", completed:false}).then(console.log, console.error);
// 修改Todo记录
http.put('/classes/Todo/57e5c7b78ac247005bc28e82', {completed:true}).then(console.log, console.error);
// 删除Todo记录
http.delete('/classes/Todo/57e5c7b78ac247005bc28e82').then(console.log, console.error);
这里需要注意的是,由于LeancloudHttp是基于**fetch api**的,所以在没有fetch的环境(例如Node)中使用时,需要require一个fetch的polyfill。github的 whatwg-fetch 是个不错的选择。
2. 作为API封装工具,让开发者使用流畅API来访问Leancloud的服务
使用LeancloudHttp虽然可以很方便地使用 GET | POST | PUT | DELETE 方法调用LeanCloud的RESTFUL API,但是还是需要写很多代码。所以在LeancloudHttp的基础上我们提供了一种基于模版声明的方法帮助开发者定义API。这是fluent-leancloud和官方SDK最大的区别。我个人认为Declarative的编程方式比Imperative编程方法能够更方便简洁地描述API。
Declarative programming is “the act of programming in languages that conform to the mental model of the developer rather than the operational model of the machine”.
以LeanCloud的存储服务为例,所有的数据表都提供了相同的CRUD操作方法,所以我们可以定义一个resource模版来描述这些方法。而不用每次显性地呼叫操作流程来完成操作。事实上, fluent-leancloud所有的api方法都是通过模版的方式定义的,用户的自定义方法也是通reduce已有模版完成。
还是以Http Client中的Todo为例子
//Node环境下必须提供一个fetch polyfil
import 'whatwg-fetch';
import {LeancloudHttp} from 'fluent-leancloud';
// 创建一个Http client, 和之前示例一致,此处省略...
const http = ...
// 创建 API factory
const {factory} = LeancloudApi(http)
// 创建 Todo Api Object.
const Todo = factory({type: 'Todo'})
// --------------------
// Collection 方法
// --------------------
// 创建一条Todo
Todo.create({content: "演示TODO", completed:false}).then(console.log, console.error);
// 查找Todo
Todo.find({where: {completed: false}, limit:2, order:'createdAt'}).then(console.log, console.error);
// 查找第一条满足条件的记录
Todo.findOne({where: {completed: false}}).then(console.log, console.error);
// 计数
Todo.count().then(console.log, console.error);
// --------------------
// Instance 方法
// --------------------
// 实例的objectId
const objectId="5811e206a0bb9f0061e22250";
// 获取Todo的数据实例
Todo(objectId).get().then(console.log, console.error);
// 更新Todo的数据实例
Todo(objectId).update({completed: true}).then(console.log, console.error)
// 将Todo的viewCount字段加2
Todo(objectId).increase('viewCount', 2).then(console.log, console.error);
// 删除Todo的数据实例
Todo(objectId).destroy().then(console.log, console.error);
通过LeancloudApi方法,我们将一个http client封装成一个factory方法,这个factory方法被用于创建应用的Api Object。默认情况下,factory方法使用resource模版构建一个Api对象。通过这个Api对象我们可以使用流畅Api的方式操作数据表。这些操作被分成两类。
- Collection方法是针对整个数据表的操作,包括(create, find, findOne 和 count)。 调用方法类似于static方法,例如
Todo.create(data)
- Instance方法是针对某个数据项的操作,包括(get, update, destroy 和 increase)。调用方法为函数链接,例如
Todo(id).update(data)
我们可以基于内建模版增添新的方法函数,或者彻底使用其它模版构建方法函数。下面就是增添自定义方法的示例,在这个场景中,我们希望通过实例方法为一个Todo添加标签。
//Node环境下必须提供一个fetch polyfil
import 'whatwg-fetch';
// 导入 LeancloudHttp, LeancloudApi
import {LeancloudHttp, LeancloudApi} from 'fluent-leancloud';
// 导入Array操作
import {Array} from 'fluent-leancloud/dist/FieldOps';
// 创建一个Http client, 和之前示例一致,此处省略...
const http = ...
// 创建 API factory
const {factory} = LeancloudApi(http);
// 创建 Todo Api Object
const Todo = factory({type: 'Todo'}, (todo)=>{
// 在instance上声明定制的addTags方法
todo.instance({
addTags:{
verb: 'put',
args: ['labels'],
data: ({id, labels})=>({id, tags: Array.addUnique(labels)})
}
})
})
// --------------------
// instance 方法
// --------------------
// 实例的objectId
const objectId="5811e206a0bb9f0061e22250";
// 为Todo数据项的tags字段添加一个‘work’标签
Todo(objectId).addTags(['work','programming']).then(console.log, console.error);
3. 作为ORM数据建模工具使用,同样使用流畅API降低使用时的学习成本
对于复杂一些的数据结构,fluent-leancloud提供了ORM的工具,方便开发者声明数据间的关系。目前提供了常用的**blongsTo**和**hasMany**的关系声明,由于LeanCloud的储存服务提供Pointer和Relation两种数据类型。按照官方的说明,我们默认使用Pointer来定义one to many的关系,用Relation来定义many to many的关系。
HasMany by pointer 和 BelongsTo 关系
在此先以Post和Comment为例,它们间的关系如官方文档中描述的一致,我们在Comment数据表上添加了post字段,这个字段的类型为Pointer指向一个Post
//Node环境下必须提供一个fetch polyfil
import 'whatwg-fetch';
// 导入 LeancloudHttp, LeancloudApi
import {LeancloudHttp, LeancloudApi} from 'fluent-leancloud';
// 创建一个Http client, 和之前示例一致,此处省略...
const http = ...
// 创建 API factory
const {factory} = LeancloudApi(http)
// 创建 Post Api Object.
const Post = factory({type: 'Post'}, (post)=>{
// Post hasMany comments by pointer via comments relation
post.hasMany('comments', {type:'Comment', by:'pointer', foreignKey:'post'})
});
// 创建 Comment Api Object.
const Comment = factory({type: 'Comment'}, (comment)=>{
// Comment belongs to Post via post relation
comment.belongsTo('post', {type:'Post'})
});
// 创建一条Post记录
Post.create({title:'Test'}).then(console.log, console.error);
// fake post id
const postId = "5817091567f3560058686e00";
//--------------------------------------------------
// Post(postId).comments 是个 hasMany by pointer 关系
// 这个关系有create, find, count方法
//--------------------------------------------------
// 创建一条Post的Comment记录,Comment的post字段会包含指向这个Post的指针
Post(postId).comments.create({content:"it is good"}).then(console.log, console.error);
// 查找这个Post的所有Comment记录
Post(postId).comments.find({order:"createdAt"}).then(console.log, console.error);
// 这个Post的comments总数
Post(postId).comments.count().then(console.log, console.error);
// fake comment id
const commentId = "581709658ac247004fbf50c5"
//--------------------------------------------------
// Comment(commentId).post 是个 belongsTo 关系
// 这个关系有get, set方法
//--------------------------------------------------
// 设置这条Comment的Post
Comment(commentId).post.set('5817091567f3560058686e00').then(console.log, console.error)
// 获得这条Comment的Post数据
Comment(commentId).post.get().then(console.log, console.error)
- hasMany by pointer关系定义了3个方法,分别是
find
, count
, create
- belongsTo关系定义了2个方法,分别是
get
, set
用于设定Pointer
HasMany by relation 关系
除了使用Pointer外,我们还可以使用LeanCloud提供的Relation作为构建hasMany关系的方法。以LeanCloud内建的Role为列,它包含了两个relations,一个是users, 一个是roles。
我们可以这样定义:
...
// 创建 API factory
const {factory} = LeancloudApi(http)
// 定义 Role Api
const Role = factory({type:'_Role'}, role=>{
role.hasMany('users', {type: '_User', by: 'relations'});
role.hasMany('roles', {type: '_Role', by: 'relations'});
})
使用示例:
...
async function seedRolesExample(){
// 创建 admin role
const {objectId: admin} = await Role.create({name:'admin', ACL:{"*":{read: true}}});
// 创建 manager role
const {objectId: manager} = await Role.create({name:'manager', ACL:{"*":{read: true}}});
// 将 manager role 添加到 admin roles
await Role(admin).roles.add(manager);
return {admin, manager}
}
async function userRelationExamples(roleId, userId){
// 将用户添加到一个Role中
await Role(roleId).users.add(userId);
// 将用户从一个Role中移除
await Role(roleId).users.remove(userId);
// 查询一个Role下的所有用户
await Role(roleId).users.find({where:{verified:false}});
// 查询一个Role下的所有用户数量
await Role(roleId).users.count();
}
// 执行 seedRolesExample
seedRolesExample().then(console.log, console.error)
这上面的示例中我们定义了一个hasMany by relations的关系,定义方法为 role.hasMany('users', {type: '_User', by: 'relations'});
。 目前hasMany by relations关系定义了4个方法,分别是 find
, count
, add
, remove
我们会根据需要在后续的版本中加入hasMany by through的关系,用于描述多对多关系中的另一边,在_Role这个场景中,我们可以在_User端定义user.hasMany('roles', {type:'_Role', by: 'through'})
的关系。
需要注意的是我们用下划线来表明LeanCloud内部的数据表,比如_User和_Role。
最佳实践 Best Practice
关于Store (MemoryStore, FileStore, SessionStore)
(补充文档)
Node Express 和 KOA 整合
(补充文档)
React Redux Flux 整合
(补充文档)
React Native 注意事项
(补充文档)
内建模型
User
(补充文档)
Sms
(补充文档)
Push
(补充文档)