移动后端服务BaaS现状

原创文章,转载请注明:转载自Keegan小钢
微信订阅号:keeganlee_me
写于2012-11-10



开发一个具有网络功能的移动应用,除了要开发客户端,还要开发服务端,还需要服务器。为了简化移动应用的开发和部署,让开发者只专注于客户端的开发,而将后端服务整合成API提供给开发者调用,这就是BaaS(Backend as a Service)。

目前,国外至少已经有二十多家企业进入了这个领域,其中,提供的后端服务比较全面的有StackMob、Parse、Kinvey。而国内,据我所知的,到目前为止只有三个平台,Bmob、AMTBaaS,还有我们的Xone。

StackMob

StackMob算是最早进入该领域的,成立于2010年1月,去年5月份的时候,那时该团队还只有6个人,就获得了750万美元的投资。目前该团队有24个人,每月访问量已经超过300万,使用其服务的应用已经达到120万。支持的SDK已经涵盖了iOS、Android、JavaScript、Ruby,还有Custom Code。其中,Custom Code是用于服务端开发的,可以用Java和Scala语言进行开发。通过Custom Code,开发者就可以在服务端处理一些无法在客户端处理的逻辑。StackMob提供的功能服务也挺全面的,大的方面,数据存储、推送服务、地理位置服务、社交等,再细入到支持角色管理、ACL、复杂和多层的数据模型、多种查询条件等等。

而那么多SDK中,我只看了Android的。其SDK也挺简单易用的,应用程序自身的数据模型只要继承StackMobModel即可,而用户类则继承StackMobUser,然后可以直接定义自己的属性。另外,它也支持嵌套的数据模型,最多支持3层嵌套。比如下面的代码:

public class Task extends StackMobModel {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    private String name;
    private Date dueDate;
    private int priority;
    private boolean done;
    private Task prerequisite;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public Task(String name, Date dueDate) {
        super(Task.class);
        this.name = name;
        this.dueDate = dueDate;
        this.priority = 0;
        this.done = false;
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public void setPrerequisite(Task prereq) {
        this.prerequisite = prereq;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
public class TaskList extends StackMobModel {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    private String name;
    private List<Task> tasks;
    private Task topTask;
    private TaskList parentList;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public TaskList(String name) {
        super(TaskList.class);
        tasks = new ArrayList<Task>();
        this.name = name;
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public String getName() {
        return name;
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public List<Task> getTasks() {
        return tasks;
    }
}

保存数据时,代码就可以如下:

TaskList taskList = new TaskList("StackMob Tasks");
taskList.getTasks().add(new Task("Learn about relations", new Date()));
taskList.getTasks().add(new Task("Learn about queries", new Date()));
taskList.save(StackMobOptions.depthOf(1));

StackMob的这种模式,简单易懂,操作上也很方便,数据模型的扩展性也很好。

不过,文件存储就没这么方便了,StackMob不提供将文件上传到它自己的平台服务器,只提供接口,让你将文件上传到开发者自己的S3上面。也就是说,开发者必须自己先申请AWS的S3,才可以使用StackMob提供的文件存储服务。

还有,StackMob的推送服务是使用Google的GCM服务的,所以,开发者还需要拥有Google的GCM账号才能注册使用StackMob的推送服务。

另外,其Model和Controller的分离也还不够好。例如,对于用户操作的方法,在StackMobUser里提供了很多方法,但在StackMob里也提供了不少方法。如果将数据操作的部分可以统一接口就更好了。

再看看StackMob提供的Custom Code,它也提供了操作后台数据很全面的各种方法和类。使用起来也挺简单,比如写个hello_world的例子,后台服务类需要实现CustomCodeMethod接口,比如下面代码:

public class HelloWorldExample implements CustomCodeMethod {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
  /**
   * This method simply returns the name of your method that we'll expose over REST for
   * this class. Although this name can be anything you want, we recommend replacing the
   * camel case convention in your class name with underscores, as shown here.
   *
   * @return the name of the method that should be exposed over REST
   */
  @Override
  public String getMethodName() {
    return "hello_world";
  }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
  /**
   * This method returns the parameters that your method should expect in its query string.
   * Here we are using no parameters, so we just return an empty list.
   *
   * @return a list of the parameters to expect for this REST method
   */
  @Override
  public List<String> getParams() {
    return Arrays.asList();
  }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
  /**
   * This method contains the code that you want to execute.
   *
   * @return the response
   */
  @Override
  public ResponseToProcess execute(ProcessedAPIRequest request, SDKServiceProvider serviceProvider) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
    //Send push messages...
    //Query the datastore...
    //Run complex server side operations..
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
    //Then prepare your custom JSON to send back to the mobile client
    Map<String, String> args = new HashMap<String, String>();
    args.put("msg", "hello world!");
    return new ResponseToProcess(HttpURLConnection.HTTP_OK, args);
  }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
}

然后,在Android端这样调用:

StackMob.getStackMob().getDatastore().get("hello_world", new StackMobCallback() {
    @Override public void success(String responseBody) {
        //responseBody is "{ \"msg\": \"Hello, world!\" }"
    }
    @Override public void failure(StackMobException e) {
    }
});

StackMob教程指南:https://developer.stackmob.com/tutorials

Parse

Parse比StackMob晚成立一年,近期也得到了700万美元的注资,目前团队20人,有2万名开发者使用其平台,40万款应用使用其服务。支持的API有iOS、Android、JavaScript、Windows 8,以及提供了REST API和Cloud Code JS API。Cloud Code JS API和StackMob的Custom Code一样用于服务端开发,处理自己的逻辑,不同的是,StackMob用Java或Scala开发,而Parse提供JS的接口。Parse提供的功能服务也算是比较全面的,StackMob提供的大部分功能服务,它也提供了。而支持Windows8,这点还比StackMob领先了一步。另外,它对ACL的设置也比StackMob灵活。StackMob只能在后台管理页面对角色设置ACL,而Parse可以在代码中设置全局的ACL,也可以对每个对象设置ACL。文件存储方面,Parse虽然也是保存在S3,但它将用户的文件保存在自己的平台,而不是开发者自己的S3,这一点也比StackMob要方便得多。

不过,Parse的SDK使用起来就没StackMob那么方便了。Parse保存和查询对象的代码如下:

ParseObject gameScore = new ParseObject("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.saveInBackground();
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
ParseQuery query = new ParseQuery("GameScore");
query.getInBackground("xWMyZ4YEGZ", new GetCallback() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      int score = object.getInt("score");
      String playerName = object.getString("playerName");
      boolean cheatMode = object.getBoolean("cheatMode");
    } else {
      // something went wrong
    }
  }
}

这样子,开发者就很难定义自己的数据模型,全部操作都只能通过ParseObject了。Model和Controller完全耦合在一起,应用与Parse的耦合性非常强,如果开发者想要移植到其他平台就会很麻烦。而且,这种模式,对于开发比较复杂的应用也将变得麻烦。

再看看Parse的Cloud Code。使用Parse的Cloud Code,需要下载Parse提供的命令行工具,然后需要用parse new命令创建一个存放代码的目录,还需要输入邮箱和密码登录开发者的账号,选择一个应用,并用cd命令进入创建的目录:

$ parse new MyCloudCode
Email: ninja@gmail.com
Password:
1:MyApp
Select an App: 1
$ cd MyCloudCode

MyCloudCode目录会自动生成几个文件:

-config/
  global.json
-cloud/
  main.js

其中,main.js就是保存开发者自己所有Cloud Code的地方。写个最简单的例子:

Parse.Cloud.define("hello", function(request, response) {
  response.success("Hello world!");
});

然后用parse deploy命令部署到Parse平台上:

$ parse deploy

然后,在Android端就可以用以下代码调用:

ParseCloud.callFunction("hello", new HashMap<Object, Object>(), new FunctionCallback<String>() {
   void done(String result, ParseException e) {
       if (e == null) {
          // result is "Hello world!"
       }
   }
});

Parse文档:https://parse.com/docs

Kinvey

Kinvey则是TechStars下的一家创业公司,也是去年成立,到目前为止也已经得到了700万美元的投资,目前团队19人,而使用其平台的应用数则没有公布。前端支持的API有iOS、Android、JavaScript和REST,后端同样提供了Backend Logic接口供开发者处理后台逻辑。相较于StackMob和Parse,Kinvey提供的功能就没那么全面了,缺少了角色管理,也缺少了地理位置服务。

Kinvey的SDK有一点我比较喜欢,那就是Model和Controller有了较好的分离,Model提供了接口MappedEntity,Controller则用KCSClient做入口。不过,数据模型的定义却不如StackMob那样方便,多了一步属性与后台表字段的映射。代码如下:

public class MyEntity implements MappedEntity {
    private String uname;
    private String id;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    @Override
    public List<MappedField> getMapping() {
        return Arrays.asList(new MappedField[] { new MappedField("uname", "name"),
                                                new MappedField ("id", "_id") });
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    // Getters and setters for all fields are required
    public String getUname() { return uname; }
    public void setUname(String n) { uname = n; }
    public String getId() { return id; }
    public void setId(String i) { id = i; }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
}

其中,getMapping方法就是处理映射关系。然后保存数据就可以通过以下代码:

MyEntity item = ...;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
item.setName("Idle Blue");
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
mKinveyClient.mappeddata("collectionName").save(item, new ScalarCallback<MyEntity>() {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
  @Override
  public void onFailure(Throwable e) { ... }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
  @Override
  public void onSuccess(MyEntity r) { ... }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
});

其中,mKinveyClient.mappeddata("collectionName")方法得到MappedAppData,该类封装了MappedEntity相关的各种操作。另外,通过mKinveyClient.resource("name")方法则得到KinveyResource,该类封装了文件相关的各种操作。

Kinvey另外还提供了和ParseObject类似的方式,代码如下:

EntityDict album = mKinveyClient.entity("albums");
album.putProperty("title", "Idle Blue");
album.save(new ScalarCallback<EntityDict>() {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
    @Override
    public void onFailure(Throwable e) { ... }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
    @Override
    public void onSuccess(EntityDict r) { ... }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
});

Kinvey的Backend Logic与StackMob和Parse有着很大不同。StackMob和Parse虽然处理方式不同,但基本思路都是前端传送指定的方法(如hello_world),后台写相应的代码处理这个方法。而Kinvey的Backend Logic则是在对数据库操作的前后分别提供接口处理。如下图:

PreProcess提供了几个方法:

function onPreSave(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
function onPreDelete(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
function onPreFetch(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
}

PostProcess也提供了几个方法:

function onPostSave(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
function onPostDelete(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
function onPostFetch(request,response,modules){
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
}

Kinvey文档:http://docs.kinvey.com/overview.html

Bmob

Bmob不是一家公司的名字,只是广州一家叫鹏锐的IT公司的其中一个产品。Bmob应该算是国内第一个BaaS平台,今年4月份的时候上线的。而到目前为止,Bmob还只有Android的SDK,而且还是山寨Parse的,但功能比Parse少了很多,没有角色管理,没有ACL,也没有关系查询,更没有后端的Cloud Code,跟Parse的SDK比较一下,就会知道相比Parse少了非常多的东西,基本上就是Parse的一个简化版。版本升级过一次,在第二版加入了支付功能,而支付卡基本上都是些游戏卡,很明显,这是为付费游戏提供的服务。

看看Bmob的代码,和Parse一样的吧:

BmobObject gameScore = new BmobObject("GameScore");
gameScore.put("score", 1200);
gameScore.put("playerName", "张小明");
gameScore.put("cheatMode", false);
try {
    gameScore.save();
} catch (BmobException e) {
    // e.getMessage() 捕获的异常信息
}

Bmob第一版的时候,就出现过很多问题,如经常断线,数据丢失等,逐渐失去开发者的信任,基本功能也还很不完善。而第二版没有去完善那些重要的功能,提高开发者的信任度,反而推出了支付功能,我觉得这是个错误的策略。首先,重点应该放在完善基本功能和提高开发者对平台的信任度;其次,支付功能这种安全性要求非常高的服务,只有在平台已经非常成熟,用户对其已经非常信任的前提下提供才会有效用,在经常断线,数据会出现丢失的情况下,有谁敢去用支付功能呢?

另外,Bmob还提供了很多应用分析的功能,这一点,我也觉得多余了。专业的应用分析,已经有友盟平台了,没必要自己再做一套。

Bmob还存在很多不成熟的地方,第一版上线到目前也已经半年多了,改善的东西很少,发展速度较慢,给我的感觉就是他们没有抓到真正的重点,或者他们将精力移到了广告平台——他们的另一个产品,毕竟那是实实在在可以赚到钱的产品。

Bmob官网:http://bmob.cn/

AMTBaaS

AMTBaaS是诚迈科技最近推出的BaaS平台,9月中旬发布了iOS的SDK,10月中旬又发布了Android的SDK。目前,处于内测阶段。提供的文档,除了API,也只有一个用户手册提供下载。网站和文档的用户体验都比较差,其SDK的体验也是很差,就说一个用户注册的方法:

register(Context context, java.lang.String useremail, java.lang.String passWord, java.lang.String confirmPwd, boolean ischeckCode, java.lang.String checkcode, AmtUserCallback amtUserCallback)

再看看用户注册的操作:

// 注册消息
public static final int MESSAGE_REGISTER = 1001;
// 构造AmtUser对象
private AmtUser amtUser = new AmtUser();
// 注册
amtUser.register(UserActivity.this, user@qq.com, 123456, 123456, true, 53Fe, new AmtUserCallback() {
    @Override
    public void onSuccess(Object object) {
        if (object != null) {
            // 回调方法中不可直接操作UI控件
            mhandler.sendMessage(mhandler.obtainMessage(
                MESSAGE_REGISTER, object));
        }
    }
                                                                                                                                                                                                                                                                    
    @Override
    public void onFailure(AmtException amtException) {
        if (amtException != null) {
            // 回调方法中不可直接操作UI控件
            mhandler.sendMessage(mhandler.obtainMessage(
                MESSAGE_REGISTER, amtException));
        }
    }
});
/**
* 注册回调句柄
*/
Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case MessageTypes.MESSAGE_REGISTER:
            // 注册失败
            if (msg.obj instanceof AmtException) {
             AmtException amtException = (AmtException)msg.obj;
             AmtUtil.log("UserActivity", "注册失败:"+
                        amtException.getMessage(), "i");
            }
            // 注册成功
            else if (msg.obj instanceof String) {
             String result = (String)msg.obj;
             If (AmtConstants.SUCCESS.equals(result) {
                 AmtUtil.log("UserActivity", "注册成功", "i");
             }
            }
            break;
        }
    }
};

一个注册就这么麻烦,完全不懂用户体验。对于其前景不看好。

AMTBaaS官网:http://amtbaas.com/

Xone

Xone则是我们公司现在的核心产品,目前开发了两个月左右,前端只支持Android,目前的SDK提供的功能有版本管理、数据存储、用户管理、文件存储、地理信息服务,推送服务还没开发出来。后端也提供JS的Backend Logic服务,刚开发完。网站也只是搭了个雏形,现在正在重新设计和完善之中。

为了将Model和Controller较好地分离并能简单的使用SDK,我们采用了这样的结构:

  1. 数据模型我们定义了一个基类XoneEntity,类里只定义了几个基本属性,不带任何数据操作的方法。另外定义了继承它的两个子类,XoneGeoEntity和XoneUser,XoneGeoEntity用于带有地理信息的数据模型,XoneUser则是用户数据模型。应用程序自身的数据模型则继承XoneEntity,然后定义自己的属性即可,不需要像Kinvey那样还要做映射。如果应用程序自身的数据模型需要包含地理信息,则继承XoneGeoEntity,用户模型则继承XoneUser。

  2. 对各种服务分类提供了接口,每个接口里定义了相应的数据操作的方法。服务接口有:

    AppService:应用程序和平台相关的服务接口

    CollectionService:集合相关的服务接口

    UserService:用户相关的服务接口

    FileService:文件相关的服务接口

  3. XoneClient是Controller的入口,通过XoneClient实例的getXXXService()的各种方法调用各种服务接口,再通过服务接口调用具体的数据操作方法。比如,用户注册:

UserService<XoneUser> userService = xoneClient.getUserService();
userService.signUp(user, callback);

一切都还在完善之中,各种功能服务,以及网站。后续,当我们的平台正式上线时,我们的SDK还将会开源。

Xone官网:http://xone.im/


扫描以下二维码即可关注订阅号。

Comments
Write a Comment