在前面两篇文档中已经介绍了如何使用Clouda进行开发,这篇文档将从目录结构、文件作用、用法等方面对Clouda进行详细的介绍。

Clouda目录结构

Clouda目录结构如下图:

app:应用开发相关的代码和资源放在该目录下

app/config: 应用相关框架文件存放在该目录下

app/controller: controller全部放在该目录下

package.js: 用于将文件之间的依赖关系添加到Clouda中

app/model: Model文件存放在该目录下

app/server_config:服务器端配置文件以及不想下发到客户端的文件可存放在该目录下

app/publish: publish文件存放在该目录下(默认没有该目录,如需要可在app/下创建)

app/index.html: 应用release版本访问使用

app/debug.html: 应用debug版本时使用

app/view: view文件放在该目录下

docs:离线文档存放该文件夹下

node_modules: 模板放在该文件夹下

sumeru: 框架的文件,开发者可不关心

server_config

  • site_url.js

    配置访问应用的入口URL

  • database.js

    配置数据库的参数

  • bae.js

    如果您需要将应用部署到BAE上,需要配置该文件中得参数

    • dbname

      在BAE上申请的mongoDB数据库名

    • site_url

      在BAE上申请的URL

  • cluster.js

    多单元集群支持配置文件

  • mime.js

    mine类型配置,如果您需要添加其他类型,可以在该文件中添加和修改

  • tmp_dir.js

    如果您在应用中需要上传文件到Server,需要配置该文件的参数

    • tmp_dir

      上传文件的临时存放文件夹,值为相对于app的路径,默认为‘tmp’

Publish/Subscribe

Publish

Clouda使用PubSub模型描述数据的传输,其中,publish是发布数据的方法,其运行在Server上,每一个publish文件均需要放置在publish/。

module.exports = function(sumeru){
    sumeru.publish(modelName, publishName, function(callback){

    });
}

可以看到在sumeru.publish()中有三个参数,modelNamepublishName和一个匿名方法function(callback){},下面详细介绍这些参数的作用。

  • modelName:

    被发布数据所属的Model名称

  • publishName:

    所定义的Publish的唯一名称,在一个App内全局唯一,该参数与Controller中subscribe()成对使用。

  • function(callback){}

    描述数据发布规则的自定义函数,在这里定义被发布数据所需要符合的条件。自定义函数自身也可接受由subcribe()传入的参数,如:。

    • function(arg1,arg2,...,callback){}

      其中arg1, arg2为传入参数,传入参数的数量不限,但需要与对应的subscribe()中所传递的参数数量一致。

Subscribe

与sumeru.publish()相对应,我们在Controller中使用env.subscribe()订阅被发布的数据。其中env是Controller中很重要的一个内置对象,稍后我们还会多次见到。

env.subscribe(publishName, function(collection){

});
  • publishName:

    所定义的Publish的唯一名称,在一个App内全局唯一,该参数与sumeru.publish(modelName, publishName,function(callback))中的publishName名称需要保持一致。

  • function(collection)

    Subscribe成功获得数据时,被调用的响应方法。通常,我们主要在其中完成将订阅得到的数据与视图进行绑定(bind)的工作。

    • collection:

      订阅获得的数据Collection对象

如果需要向Publish传递参数(在上一节的最后我们曾经提到),则使用如下形式。

 env.subscribe(publishName,arg1, arg2, ..., function(collection){});

arg1,arg2...等任意数量的参数会被传入sumeru.publish()对应的function(arg1,arg2,...,callback)中。

一个Pub/Sub实例

现有一个学生信息的Model(student),假设Controller希望获取全班同学的信息,我们使用Publish/Subscribe方式实现如下:

  • Publish

    module.exports = function(sumeru){
        sumeru.publish('student', 'pub-allStudents', function(callback){
            var collection = this;
    
            collection.find({}, function(err, items){
                callback(items);
            });
        });           
    }
    
  • Subscribe

    env.subscribe("pub-allStudents", function(studentCollection){
    
    });     
    

假设我们在这个基础上加一个条件限制,现在只希望获取年龄大于18岁同学的信息。

  • Publish

    module.exports = function(sumeru){
        sumeru.publish('student', 'pub-adultStudents', function(callback){
            var collection = this;
    
            collection.find({"age":         
                                {$gt:18}
                             }, function(err, items){
                callback(items);
            });
        });           
    }
    

    大家可以看到我们使用了{"age":{$gt:18}}的方式表达了“年龄大于age”的约束要求。

    相似的,“年龄小于18”的表达方式如下:

    {"age":
        {$lt:18}
    }
    

    “大于min且小于max”的表达方式如下:

    {"age":
        {$gt:min},
        {$lt:max}
    }
    

    支持的操作符如下:

    操作符 含义
    $gt 大于
    $lt 小于

    对应的Subscribe如下

  • Subscribe

    env.subscribe("pub-adultStudents",function(studentCollection){
    
    }); 
    

我们在上面的方式上再加一个条件,现在需要大于18岁男生或者女生的信息,性别由Subscribe来决定,如何实现呢?

  • Publish

    module.exports = function(sumeru){
        sumeru.publish('student', 'pub-adultStudentsWithGender', function(gender,callback){
            var collection = this;
    
            collection.find({"age":{$gt:18}, 
                              "gender": gender
                             }, function(err, items){
                callback(items);
            });
        });           
    }
    

    在这里可以看出所发布的学生的性别,是由Subscribe决定的。这样来看,一个Publish,可以通过不同的参数,为多个Subscribe服务。从这个角度来讲,Publish有点类似于OO语言中的Class的概念,可以理解为Publish发布的是一类数据。

    类似的,对应的Subscribe调用如下:

  • Subscribe

    env.subscribe("pub-adultStudentsWithGender","male",function(msgCollection){
    
    });
    

external

Clouda提供了三方数据同步的方法,用来满足从第三方网站/第三方接口获取和同步数据的需求。下面将通过一个例子来说明一次三方数据同步的过程。

1. 定义第三方数据Model

在抓取第三方数据之前,先定义一个三方数据的Model用于描述抓取后数据的结构,在app/model目录下定义model,三方model定义与普通model定义完全一致

Model.student = function(exports){
    exports.config = {
        fields : [
            { name : 'name', type : 'text'},
            { name : 'age', type : 'int', defaultValue : 0}
        ]
    }
}

2. 指定三方数据来源与解析方法

app/publish/下新增externalConfig.js文件(文件名任意,推荐使用externalConfig.js),用来指定第三方数据来源与解析方法,需要注意必须为抓取回来的数据指定一个唯一标识uniqueColumn

/**
 * 获取第三方数据信息,由开发者自定义
 */
function runnable(){
    //{Object} config是所有三方publish配置的容器
    var config = {};

    config['pubext'] = {
        //{String} uniqueColumn为三方数据唯一标识
        uniqueColumn : "name",

        //method:GET/POST,暂不支持PUT和DELETE方法,默认请求方式为GET
        method:"POST",

        //{Function} fetchUrl的参数就是订阅时发起的参数,返回值为pubext所抓取的url地址
        fetchUrl : function(/** arg1, arg2, arg3 */){
            return 'http://some.host.com';
        },

        //{Function} resolve方法作用是将抓取回来的原始数据(originData)转化成为符合Model定义的数据(resolved)
        resolve : function(originData){
            var j = JSON.parse(originData);
            var resolved = j;

            return resolved;
        },

        //{Number} fetchInterval为可选参数,用来指定抓取时间间隔,单位为ms
        fetchInterval : 60 * 1000,

        //{Boolean} buffer为可选参数,值为true时表示获取原始Buffer,否则获取原始数据字符串
        buffer : false
    }

    //最后需要声明此模块为归属为'external'
    return {
        type : 'external',
        config : config
    }

}

module.exports = runnable;

如果method使用的是POST方法,那么需要将request body为subscribe的倒数第二个参数,如无参数,默认POST请求的request body为空字符串""; 添加request body时,同时需要为publish方法增加参数。

  • subscribe

    function getExt() {
        var requestBody = {test : 1};
        session.extStudent = env.subscribe('pubext', requestBody, function(collection, info){
            session.bind('extBlock', {
                data : collection.find().getData()
            });
        });
    }
    
  • publish

    fw.publish('student', 'pubext', function(requestBody,callback){
        var collection = this;
        collection.extfind('pubext',requestBody,callback);
    
    });
    

3. publish第三方数据

上面制定了第三方数据来源之后,就可以在publish中将其发布出来,注意这里需要使用collection.extfind方法,该方法表示此collection为三方数据,处理数据方式不同。

//与普通的collection.find不同,第三方数据的publish调用collection.extfind方法表示此collection为三方数据
fw.publish('student', 'pubext', function(/** arg1, arg2, arg3...*/ callback){
    var collection = this;
    collection.extfind('pubext', /** arg1, arg2, arg3...*/ callback);

});

4. 指定三方增/删/改接口以及数据

如想将本地数据修改同步到三方,并且三方提供相应的post接口,可以在app/publish/externalConfig.js文件中,声明三方增/删/改接口以及数据:

较为紧凑的声明方式

声明中:

  • postUrl方法用来指定三方post接口的地址信息, 参数type为增量类型,增量类型为'insert','update','delete'三者之一;

  • prepare方法用来将增量数据转化成为符合三方POST接口要求的post数据,参数type同为增量类型,参数data为增量的实际数据。

    /**
     *  三方数据POST请求信息,由开发者自定义
     */
    function runnable(){
    
        var config = {}
    
        config['pubext'] = {
    
            /**
             * 声明三方POST接口地址
             * {String} type为'delete', 'insert', 'update'其中之一
             * 如果subscribe时带参数,参数会按照subscribe顺序接在postUrl的参数中
             */
            postUrl : function(type /** arg1, arg2, arg3... */){
    
                var options = {
                    host : 'some.host.com',
                    path : '/' + type ,
                    headers: {
                        //在此自定义header内容,clouda默认的 'Content-Type': 'application/x-www-form-urlencoded'
                        'Content-Type': ...
                    }
    
                }
    
                return options;
    
            },
    
            /**
             * prepare方法将增量数据转化为符合三方要求的post数据。
             * {String} type为增量操作,值为'delete', 'insert', 'update'其一;
             * {Object} data为增量数据,如:{ name : 'user1', age : 26 }。
             */
            prepare : function(type, data){
                var prepareData = {};  //prepareData为三方post所需的data
                if(type === "delete"){
                    prepareData.name = data.name;
                }else if(type === "insert"){
                    prepareData.name = data.name;
                    prepareData.age = data.age;
                }else{
                    prepareData.name = data.name;
                    prepareData.age = data.age;
                }
                return prepareData;
            }
        }
    
        return {
            type : 'external',
            config : config
        }
    
    }
    
    module.exports = runnable;
    
较为工整的声明方式

较为工整的声明方式根据type将不同操作区分开来。

声明中:

  • deleteUrlinsertUrlupdateUrl三个方法作用等同于postUrl,返回不同操作下三方接口url信息;

  • onDeleteonInsertonUpdate三个方法作用等同于prepare方法, 返回经过处理,传给三方接口的post数据。

    function runnable(){
    
        var config = {};
    
        config['pubext'] = {
            //arg1, arg2, arg3是subscribe时输入的参数
            deleteUrl : function(/** arg1, arg2, arg3... */){
                return {
                    host : 'some.host.com',
                    path : '/delete' ,
                    headers: {
                        //在此自定义header内容,clouda默认的 'Content-Type': 'application/x-www-form-urlencoded'
                        'Content-Type': ...
                    }
                }
            },
    
            insertUrl : function(/** arg1, arg2, arg3... */){
                return {
                    host : 'some.host.com',
                    path : '/insert',
                    headers: {
                        //在此自定义header内容,clouda默认的 'Content-Type': 'application/x-www-form-urlencoded'
                        'Content-Type': ...
                    }
                }
            },
    
            updateUrl : function(/** arg1, arg2, arg3... */){
                return {
                    host : 'some.host.com',
                    path : '/update',
                    headers: {
                        //在此自定义header内容,clouda默认的 'Content-Type': 'application/x-www-form-urlencoded'
                        'Content-Type': ...
                    }
                }
            },
    
            onInsert : function(data){
                var prepareData = {};
                prepareData.name = data.name;
                prepareData.age = data.age;
                return prepareData;
            },
    
            onUpdate : function(data){
                var prepareData = {};
                prepareData.name = data.name;
                prepareData.age = data.age;
                return prepareData;
            },
    
            onDelete : function(data){
                var prepareData = {}
                prepareData.name = data.name;
                return prepareData;
            }
        }
    
        return {
            type : 'external',
            config : config
        }
    
    }
    
    module.exports = runnable;
    

5. subsribe第三方数据

subscribe方法与普通subscribe无差别,开发者只用关心所订阅的pubName,而不用区分数据来源。

function getExt() {
    session.extStudent = env.subscribe('pubext', function(collection, info){
        session.bind('extBlock', {
            data : collection.find().getData()
        });
    });
}

sumeru.external.post与sumeru.external.get接口

如果上面的方法不能满足您的需求,Clouda同样提供更底层,更灵活的post和get接口

向第三方发送get请求
var url = "http://some.host.com";
var getCallback = function(data){
    console.log(data);
}
sumeru.external.get(url, getCallback);
向第三方发送post请求
var options = {
    host : "some.host.com",
    path : "/insert"
}

var postData = {
    name : sumeru.utils.randomStr(8),
    age : parseInt( 100 * Math.random())
}

var postCallback = function(data){
    console.log(data);
}

sumeru.external.post(options, postData, postCallback);
注意:sumeru.external.post与sumeru.external.get接口在Controller中onready()生命周期中使用

详细代码和说明请参考《Examples》文档中SpiderNews实例。

sumeru.external.sync

将抓取最新的三方数据,并将新数据推送至前端

var cb = function(data){
    console.log(data);
}
var url = "some.host.com";
sumeru.external.sync(modelName, pubName, url, cb);

Controller

如果你曾经接触过MVC模型,那么将会很熟悉Controller的概念。在Clouda中,Controller是每个场景的控制器,负责实现App的核心业务逻辑。每一个Controller文件都放在controller/下。

 App.studentList = sumeru.controller.create(function(env, session){

 });

使用sumeru.controller.create()创建一个名为studentList的Controller。

在Controller中有两个非常重要的对象env和session,env用来绑定Controller的生命周期方法,session用来绑用户数据,下面详细的介绍这两个对象。

env

env用来绑定Controller的生命周期方法,Controller具有以下几个时态:onload()、onrender()、onready()、onsleep()、onresume()、ondestroy()。

  • onload

    语法:env.onload(){}
    

    onload()是Controller的第一个时态,Controller中需要使用的数据都在这个时态中加载,我们上面谈到过的subscribe()也多在这个时态中使用,方法如下。

     App.studentList = sumeru.controller.create(function(env, session){
        var getAllStudents = function(){
            env.subscribe("pub-allStudents",function(studentCollection){
    
            });
        };
    
        env.onload = function(){
            return [getAllStudents];
        };
     });
    

    注意:如果您开启了Server端渲染,那么在onload函数中需确保onload中,没有使用前端的js中的变量或函数,比如window,document,Localstorage等

  • onrender

    语法:env.onrender(){}
    

    当数据获取完成后,这些数据需要显示在视图(View)上,这个过程通过onrender()中的代码来实现,这是Controller的第二个时态,负责完成对视图(View)的渲染和指定转场方式。

    env.onrender = function(doRender){
        doRender(viewName,transition);
    };
    
    • viewName

      需要渲染的视图(View)名称。

    • transition

      定义视图转场,形式如下:

      ['push', 'left']
      

      转场方式:我们提供'none', 'push'、'rotate'、'fade'、'shake'五种转场方式

      转场方向:不同的转场方式有不同的转场方向,请参考附录:《API说明文档》

  • onready

    语法:env.onready(){}
    

    这是Controller的第三个时态,在View渲染完成后,事件绑定、DOM操作等业务逻辑都在该时态中完成;每段逻辑使用session.event包装,从而建立事件与视图block的对应关系。

     env.onready = function(){
        session.event(blockID,function(){
    
        });
     };
    
    • blockID

      View中block的id,关于block在接下View中会做详细的介绍。

    • function(){}

      事件绑定、DOM操作等业务逻辑在这里完成。例如有一个View如下:

      <block tpl-id="studentList">
          <button id="submit"> </button>
      </block>
      

      如何对view中的submit做事件绑定呢?可以通过下面代码实现:

      env.onready = function(){
          session.event("studentList",function(){
              document.getElementById('submit').addEventListener('click', submitMessage);
          });
       };
      

    在开发移动终端上的应用时常会使用到很多的手势操作,例如旋转放大拖动等等,为了方便开发者快速的集成这些手势,Clouda中内置了事件和手势库Library.touch,如何使用Library.touch请查看API手册touch部分。

  • redirect

    语法:env.redirect(queryPath,paramMap,isforce)
    

    一个Controller跳转到另一个Controller

        env.redirect('/studentList',{'class':'101'});
    
    • queryPath

      router中pattern的值

    • paramMap

      需要向跳转Controller传递的参数
      
    • isforce

      是否强制生成一个全新的Controller实例。

  • arguments

    语法:env.arguments[pattren,params1,...,paramsN]
    

    使用该方法可以获取URL中参数,例如:

    URL:http://test.duapp.com/sourcepage/params1/params2/.../paramsN
    

    Controller中定义router:

    sumeru.router.add(
        {
            pattern: '/sourcepage',
            action: 'App.SourceController'
        }
    );
    

    那么在该Controller中就可以使用env.arguments[1]到env.arguments[N]获取对应的参数。

session

如果您有数据需要绑定到Controller,可以使用session中的方法:

  • get

    语法:session.get(key)
    

    获取session中“key”的值

  • set

    语法:session.set(key,value)
    

    设置session中“key”的值

  • commit

    语法:session.commit()
    

    当你更新了session的数据时,需要根据新的session的数据更新UI是,您可以使用session.commit()就会触发数据对应视图block的更新。

Controller接收URL中的参数

  • 使用env.redirect()方法

    当一个Controller(起始Controller)跳转到另一个Controller(目标Controller)时,可以使用env.redirect()方法来实现参数的传递,方法如下:

    • 使用paramMap传递参数

      • 在起始Controller中

        env.redirect(queryPath ,paramMap);
        

        第一个queryPath: 目标Controller在router中“pattern”的值;

        paramMap:需要传递的参数

      • 目标Controller中使用session.get()方法获取参数

        sumeru.controller.create(function(env, session){
        
            session.get();
        
        });
        
    • 使用URL路径部分传递参数

      • 在起始Controller中

        env.redirect(queryPath/params1/params2);
        
      • 目标Controller中使用session.get()方法获取参数

        sumeru.controller.create(function(env, session){
        
            params1 = env.arguments[1];
            params2 = env.arguments[2];
        });
        
    • 实例

      • SourceController.js

        sumeru.router.add(
            {
                pattern: '/sourcepage',
                action: 'App.SourceController'
            }
        );
        
        App.SourceController = sumeru.controller.create(function(env, session){
                env.redirect('/destinationpage/100/200',{'a':100,'b':200});
        });
        
      • DestinationController.js

        sumeru.router.add(
            {
                pattern: '/destinationpage',
                action: 'App.DestinationController'
            }
        );
        
        App.DestinationController = sumeru.controller.create(function(env, session){
            console.log(session.get('a'));
            console.log(session.get('b'));
            console.log(env.arguments[1]);
            console.log(env.arguments[2]);
        });
        

    跳转后的URL为:http://localhost:8080/debug.html/destinationpage/200/100?a=100&b=200&

    开发者也可按照上面的URl格式来拼接一个带参数的URL,关于URL我们会在本文档URL说明部分做详细的说明。

Model

我们使用Model来定义App的数据模型,例如在model/下创建一个student.js

 Model.student = function(exports){    

 };

在"student"中添加"studentName"、"age"和"gender"三个字段:

 Model.student = function(exports){
    exports.config = {
        fields: [
            {name : 'studentName', type: 'string'},
            {name : 'age',         type: 'int'},
            {name : 'gender',      type: 'string'}
        ]
    };
 };
  • name

    字段的名称

  • type

    字段的数据类型,包括"number"、"datetime"、"string"、"object"、"array"、"model"、"collection"。

除以上两种,常用的属性还包括:

  • defaultValue

    字段的默认值

    {name: 'gender', type: 'string', defaultValue: 'male'},
    
    {name: 'number', type: 'number', defaultValue:1},
    
    {name: 'array', type: 'array', defaultValue: ["Fight Club","health club"]},
    
    {name: 'object', type: 'object', defaultValue: {a:1,b:2}},
    

    若不提供"gender"值时,则字段的默认值为"male"。

    再看一个时间的例子:

    {name: 'time', type: 'datetime', defaultValue: 'now()'}
    

    若不提供"time"值时,则字段的默认值为当前服务器时间。

  • validation

    字段的验证,validation包括以下方法:

    • length[min,max]

      字段值得长度在min-max的范围。

    • mobilephone

      必须为手机号码格式,长度为11位且必须为数字

    • required

      字段值不能为空

    • number

      字段值必须为数字

    • unique

      字段值必须唯一

    更多内置验证方法和自定义验证方法,请参考附录:《API说明文档》

  • model

    当type值为model和collection时,表示该字段包含一个指向其他model的1:1 或 1:n 的关系。 此时,需同时提供model字段以声明指向的model对象。

    {name: 'classes', type: 'model', model: 'Model.classes'}
    

Collection

Collection是Model的集合,我们之前曾使用过的subscribe()返回的结果集即是Collection。

session.studentCollection = env.subscribe("pub-allStudents",function(myCollection){

});

session.studentCollection是返回的Collection。可对数据集进行“增、删、查、改”的操作:

  • add

    语法:add()
    

    使用add()在Collection中添加一行数据。

    session.studentCollection.add({
        studentName: 'John',
        age: 18,
        gender:"male"                   
    });
    
  • save

    语法:save()
    

    save()是用于将collection的修改保存到Server,在通常情况下,调用save()方法会自动触发对应视图block的更新。

    session.studentCollection.save();
    
  • find

    语法:find()
    

    使用find()查询Collection中符合条件的所有Model。

    session.studentCollection.find();
    

    使用条件查询时,例如查找gender为“male”的Model;

    session.studentCollection.find({gender:'male'});
    
  • destroy

    语法: destroy()
    

    使用destroy()从Collection中移除数据,

    session.studentCollection.destroy();
    

    使用条件删除时,例如删除gender为“male”的Model:

    session.studentCollection.destroy({gender:'male'});
    

更多Collection API 请参考附录:《API说明文档》

View

在上一篇文档中我们介绍过Clouda的一个重要特性“随动反馈”,那么“随动反馈”是怎么实现的呢?

Controller的onload()时态里,每一个session.bind的BLOCKID,都对应View中的一个"block"标签。Clouda使用Block为粒度来标记当数据发生变化时View中需要更新的部分,使用handlebars组件作为模板引擎。

在view中使用“block”标签定义需要更新部分,并定义tpl-id

<block tpl-id="studentList">
    <p> 
        {{#each data}}
            {{this.studentName}}
        {{/each}}           
    </p>
</block>

view中的data来源于Controller中onload()时态的session.bind()

env.subscribe("pub-allStudents",function(studentCollection){
    session.bind('studentList', {
        data : studentCollection.find(),
    });
}); 

通过以上方法,我们就建立了一个基本的"随动反馈"单位,当订阅的数据发生变化时,View中对应的部分将自动更新。

Handlebars的语法非常易用,但为了更快的开发视图代码,Clouda还额外提供了便捷的工具方法

  • foreach

    用于快速遍历一个对象或数组

    语法:{{#foreach}}{{/foreach}}
    

    用法示例:

    <p id="test-foreach-caseB">
        {{#foreach customObj}}
            {{key}} : {{value}}
        {{/foreach}}
    </p>
    
  • compare

    比较两个对象

    语法:
        {{#compare a operator b}}
        {{else}}
        {{/compare}}
    

    可以使用的operator:

    operator
    ==
    ===
    !=
    !==
    <
    <=
    >
    >=
    typeof

    用法示例:

    {{#compare a "<" b}}
        a < b
    {{else}}
        a >= b
    {{/compare}}
    
    {{#compare a "typeof" "undefined"}}
        undefined
    {{/compare}}
    

    注意:当省略operator时,系统默认使用操作符 ==:

    {{#compare 1 1}}
        1 == 1
    {{/compare}}
    
  • {{$ }}

    在View中直接执行Javascript代码,并将返回结果输出在View中。

    {{$ alert("data.length"); }}
    

View之间的互相引用

  • {{> viewname}}

    在一个View中引用另一个View。

配置view加载路径

一般情况下将编写的View文件都存放在app/view文件夹下,如果编写的view文件不在app/view文件夹下,我们也提供View文件路径配置的方法,框架会在配置路径先寻找需要的View文件:

sumeru.config.view.set('path', 'path/to/');

则Clouda会在如下目录中加载视图:

app目录/path/to/view/

注意:即使是修改viewpath的情况下,在最内一侧仍然需要有一层view文件夹,如上面路径的最后部分。

Router

Router用于建立URL中pattern与Controller之间的对应关系,添加router的操作通常在Controller文件中定义。

一个Controller可以对应多个URL,一个URL只能对应一个Controller。

  • add

    语法: sumeru.router.add({pattern:'' , action:''});
    

    使用add()可以在router添加一组pattern与Controller的对于关系,方法如下:

    sumeru.router.add( { pattern: '/studentList', action: 'App.studentList' } );

    • pattern

      URL中pattern部分的值

    • action

      对应Controller的名称

通过add方法在router中添加了URL(其路径部分)和Controller的对应关系,就可以使用“localhost:8080/debug.html/studentList”运行URL(其路径部分)为"/studentList"对应的Controller。

同时我们还提供定义默认启动Controller的方法:

  • setDefault

    语法: sumeru.router.setDefault(Controller Name)

实例:

sumeru.router.setDefault('App.studentList');

在Controller中使用setDefault()后,浏览器中输入“localhost:8080/debug.html”就可以启动该Controller,不需要在URL中带路径部分。

这里使用debug.html为调试模式,关于调试模式在“URL说明”部分会作详细介绍。

为了满足加快view渲染速度的需求,Clouda加入了Server渲染的功能。server渲染默认是开启的,如果想单独禁止某个View在Server渲染,可在Router中通过server渲染开关来禁止server渲染。

sumeru.router.add({
    pattern:'/test',
    action : 'App.unittest',
    server_render:false
})

router的外部处理器

如果您使用backbone等第三方框架,或是存在已有代码根据URL的变化执行一些逻辑,那么这些需求,都可以通过注册一个router的外部处理器使其保持正常工作。

一个外部处理器的写法:

var processor = function(path){
    //do something
    return true;
}

添加一个外部处理器:

sumeru.router.externalProcessor.add(processor);

添加一个backbone的外部处理器的例子:

sumeru.router.externalProcessor.add(Backbone.Router.extend());

server_config

当您有一些配置文件以及敏感信息不想被下发到客户端时,您可以将这些文件放在server_config文件夹下。

例如在应用中使用了一些私密的ip而不想把ip下发到客服端导致安全问题,可将ip的信息配置文件放在server_config文件夹下,方法如下:

在server_config文件夹下新建ip_config.js

sumeru.config({
    secret_ip: **.**.**.**
});

并在server_config/package.js添加文件名:

sumeru.package({
   "ip_config.js"
});

这样ip_config.js文件不会被下发到客户端上,如果需要获取secret_ip的值可在server上使用下面方法:

sumeru.config.get("secret_ip");

在server_config文件夹下默认存放的文件为Clouda配置文件,包括:

  • bae.js

  • database.js

  • site_url.js

  • server_library.js

注意:上面四个文件为Clouda保留文件,开发者不可以在server_config文件夹下创建同名的文件,也不可以在上述四个文件中添加内容

Library

有的时候我们会遇到这样的麻烦,比如Model中有一个数据类型为“date”的时间字段,而在View上我想显示的是年,我们可以在View使用{{$ }}方法嵌入JavaScript来实现。

虽然这种方法可以实现,但是不易代码管理,我们需要一个library库的管理机制来解决这个问题,例如你可以将这个时间格式化函数存放在library/下:

  • /library/getTime.js

    Library.timeUtils = sumeru.Library.create(function(exports){    
        exports.formatDate = function(time){
            return time.getFullYear();
        };
    
    });
    
  • /view/student.html

    <block tpl-id="studentList">
    
        <p> 
            {{#each data}}
                {{$Library.timeUtils.formatDate(this.time)}}
            {{/each}}           
        </p>
    
    </block>
    

也可以在controller中调用library库,例如:

  • /controller/student.js

    session.bind('studentList', {
        year : Library.timeUtils.formatDate(time)
    });
    

通常,在onload,onrender和视图文件中使用到的新增加的Library或Handlebars Helpers,都需要同时配置在server_config/server_library中,方法如下:

打开server_config/server_library.js

sumeru.packages('../library/handlbars_helper.js');

Server渲染

当您觉得view加载很慢不能满足要求,需要加快view渲染速度时,您可以使用server渲染来达到这种效果。server渲染默认是开启

在使用server渲染时需要注意在controller的onload()中不能包含window,document,Localstorage等浏览器特有的DOM和BOM操作,如果使用了server渲染可以将这些操作放在onready()中完成。

如果您的业务确实需要在onload()中使用window,document,Localstorage等浏览器特有的DOM和BOM操作,可以通过server渲染开关来关闭该功能

  • 全部关闭(所有的controller都不会使用server渲染),当需要全部禁止时,修改config/sumeru.js中,添加一行

    sumeru.config({
        runServerRender:false
    })
    
  • 单独禁止某个View在Server渲染,可在Router中添加

    sumeru.router.add({ pattern:'/test', action : 'App.unittest', server_render:false })

Manifest

Clouda框架会将各个package.js中描述的JS和CSS资源自动写入manifest文件形成离线缓存。 如果对于图片,音乐等其他文件也有离线缓存需求,可通过建立app.manifest文件进行描述。 在app.manifest中描述过的资源,Clouda框架在启动时会一并写入整体manifest文件中。

app.manifest文件应该建立在如下位置,与controller,publish等目录平级:

app/app.manifest

app.manifest文件的格式与w3c规定的manifest文件格式一致,见:http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html

注意:目前暂不支持SETTINGS:域

一个示例:

CACHE MANIFEST
# the above line is required

# this is a comment
# there can be as many of these anywhere in the file
# they are all ignored
  # comments can have spaces before them
  # but must be alone on the line

# blank lines are ignored too

# these are files that need to be cached they can either be listed
# first, or a "CACHE:" header could be put before them, as is done
# lower down.
images/sound-icon.png
images/background.png
# note that each file has to be put on its own line

# here is a file for the online whitelist -- it isn't cached, and
# references to this file will bypass the cache, always hitting the
# network (or trying to, if the user is offline).
NETWORK:
comm.cgi

# here is another set of files to cache, this time just the CSS file.
CACHE:
style/default.css

URL说明

URL是Web app中很重要的部分,在router部分介绍过URL中pattern与controller的关系,在这节将详细介绍Clouda中URL的格式和使用。

Clouda中URL的组成格式如下:

域名/{controller}/{arguments[1]}/{arguments[2]}/...?params1=string&params2=string
  • controller

    与router中的pattern对应

  • arguments

    URL中的传递参数

  • params

    与controller中使用env.redirect(queryPath ,paramMap)传递的paramMap对应

一个URL格式的实例:

URL: localhost:8080/debug.html/studentList/index/123/007?p=2

对应router定义为:

sumeru.router.add{
    {
       pattern: '/studentList/index',
       action: 'App.studentList'
    }
}

上面介绍了Clouda中URL的组成规则,那么Clouda是如何解析URL呢?下面介绍Clouda的解析方法。

  • 自动匹配到 /controller是/studentList/index

  • 将后面的参数 /123/007 作为 arguments传入env.arguments

  • 将p传入session和controller的params参数中

在上面的实例中看到URL中带有参数,那么如何获取URL中的参数呢?可按照下面方法获取:

  • env.arguments["/studentList/index","123","007"]

  • session.get('p')或者通过上面Controller之间传参部分中的params.p获取;

一般在开发阶段需要在浏览器中对应用进行debug,看到应用的源码;而在开发完成发布后,不希望别人看到应用的源码,为了满足开发者的这种需求,Clouda有调试模式和正式模式。

  • 调试模式

    使用debug.html访问进入调试模式,在调试模式下可以看到工程的源码,方便在浏览器中进行调试

    localhost:8080/debug.html/studentList/index/123/007?p=2
    
  • 正式模式

    使用index.html访问进入正式模式,在正式模式下看到的是经过编译后代码

    localhost:8080/index.html/studentList/index/123/007?p=2
    

File Uploading

Clouda提供了从端到云上传文件的方法,如果您的应用需要上传文件到云端,可以使用简单几步的配置即可实现端上的文件上传到托管服务器。

router定义上传

首先我们需要对上传进行配置,在app/config/router.js中编写下面代码。

sumeru.router.add({
    pattern    :   '/files',
    type  :   'file',
    max_size_allowed:'500k',
    file_ext_allowed:'' ,
    upload_dir:"upload",
    rename:function(filename){
        return filename+"_haha";
    }
});
  • pattern

    用于定义匹配上传文件的uri,与端上fileUploaderrouterPath对应

  • type

    文件类型

  • max_size_allowed

    允许上传文件的大小,以km为单位

  • file_ext_allowed

    支持上传文件的扩展名,包括'jpg','gif','png','ico'

  • upload_dir

    云端存放上传文件的目录名

  • rename:function(filename){}

    对上传文件重命名的方法,开发者根据自己的业务需要编写该函数

浏览器端上传文件

当服务器端配置好上传的参数后,需要在端上完成上传文件的初始化。

var myUploader = Library.fileUploader.init({
    routerPath:"/files",
    onSuccess:function(urlLink){//成功之后的处理,此处有保存文件的逻辑

    },
    fileSelect:function(e){//用户选择文件之后的处理

    },
    onProgress:function(e){//进度更新

    },
    onError:function(e){//出错

    },
    onAbort:function(e){//中断

    },
});
  • routerPath

    与router中的pattern对应

  • onSuccess:function(e)

    成功之后的处理,此处有保存文件的逻辑

  • fileSelect:function(e)

    用户选择文件之后的处理

  • onProgress:function(e)

    进度更新

  • onError:function(e)

    当上传出错时在该方法中处理

  • onAbort:function(e)

    当出现中断时在该方法中处理

当初始化完成后,就可以myUploader.startUpload()方法实现上传了。

注意

如果应用是部署在BAE上时需要在BAE上开启NFS功能,才能保证用户上传的文件不会发生丢失的情况,开启NFS后,代码+上传文件的总体积上限为100M,且有每分钟写操作不能超过50次的限制。

如果大面积使用文件上传功能,建议自建服务器

附:BAE的NFS的开启步骤和限制说明 http://developer.baidu.com/wiki/index.php?title=docs/cplat/rt/manage/nfs

一个简单的完整的上传头像demo

app/view/目录下编写uploadView.html

<block tpl-id="user_table">
    <ul>
    {{#each data}}
        <li>
            <span>{{name}}</span>
            <span>{{phone}}</span>
            <span><img src="{{photo}}"  alt="photo" width="100" height="100"/></span>
        </li>
    {{/each}}
    </ul>
</block>
<form id="upload_form" enctype="multipart/form-data" method="post">
    <div>
        <div><label for="myfile1">Please select image file</label></div>
        <div><input type="file" name="myfile1" id="myfile1" /></div>
    </div>
    <div>
        <input type="button" id="startupload" value="Upload" />
    </div>
    <div id="fileinfo">
        <div id="filename"></div>
        <div id="filesize"></div>
        <div id="filetype"></div>
        <div id="filedim"></div>
    </div>
    <div id="error">You should select valid image files only!</div>
    <div id="error2">An error occurred while uploading the file</div>
    <div id="abort">The upload has been canceled by the user or the browser dropped the connection</div>
    <div id="warnsize">Your file is very big. We can't accept it. Please select more small file</div>

    <div id="progress_info">
        <div id="progress"></div>
        <div id="progress_percent">&nbsp;</div>
        <div class="clear_both"></div>
        <div>
            <div id="speed">&nbsp;</div>
            <div id="remaining">&nbsp;</div>
            <div id="b_transfered">&nbsp;</div>
            <div class="clear_both"></div>
        </div>
        <div id="upload_response"></div>
    </div>
</form>
<div>
    <ul>
        <li>用户名:<input type="text" id="user_save_name"/> </li>
        <li>电话:<input type="number" id="user_save_phone"/> </li>
        <li><button id="user_save_button">保存</button></li>
    </ul>
    <input type="hidden" name ="hidden_file" id ="hidden_file" /><!--存放存储的用户头像-->
    <img id="preview" />
</div>

app/config/router.js中添加上文件的配置

sumeru.router.add(
    {
        pattern    :   '/files', //pattern用于定义匹配上传文件的uri
        type  :   'file',
        max_size_allowed:'500k',//support k,m
        file_ext_allowed:'' ,//allow all use '' , other use js array ["jpg",'gif','png','ico']
        upload_dir:"upload",//default dir is public
        rename:function(filename){//if rename_function is defined,the uploaded filename will be deal with this function.
            return filename+"_haha";
        }
    },
    {
        pattern    :   '/myupload',
        action  :   'App.upLoad',
        server_render : true
    }
);

app/model/目录下编写uploadModel.js

Model.userModel = function(exports){
    exports.config = {
        fields: [
            {name: 'name', type: 'string'},
            {name: 'phone', type: 'string'},
            {name: 'photo',type: 'string'}
        ]
    };
};

app/publish/下编写uploadPublish.js

module.exports = function(fw){
    fw.publish('userModel', 'user-has-photo', function(callback){
        var collection = this;
        collection.find({},function(err, items){
             callback(items);
         });
     });
}

app/controller/下编写uploadController.js

App.upLoad = sumeru.controller.create(function(env, session, params) {
    var fw = sumeru;
    var view = 'uploadView';

    var getUsers = function(){

        session.pubuser = env.subscribe("user-has-photo",function(collection, info){
            session.bind('user_table', {
                data    :   collection.getData()//collection.find({'int >=': session.get('int')})
            });
        });
    };

    env.onload = function() {
        return [getUsers];
    };

    env.onerror = function() {
        env.start();
    };

    env.onrender = function(doRender) {
        doRender(view);
    };
    env.onready = function(doc) {
        var myUploader = Library.fileUploader.init({
            routerPath:"/files",
            form:document.getElementById("upload_form"),
            // target:document.getElementById("myfile1"),
            onSuccess:function(urlLink){//成功之后的处理,此处有保存文件的逻辑
                var oUploadResponse = document.getElementById('upload_response');
                oUploadResponse.innerHTML = urlLink;
                oUploadResponse.style.display = 'block';
                document.getElementById('hidden_file').value = urlLink;//用于用户使用/保存
            },
            fileSelect:function(e){//用户选择文件之后的处理
                var oFile = e.target.files[0];
                var oImage = document.getElementById('preview');
                // prepare HTML5 FileReader
                var oReader = new FileReader();
                oReader.onload = function(e){
                    if (oFile.type == 'image/jpeg'){
                        oImage.src = e.target.result;
                        oImage.onload = function () { // binding onload event
                            console.log(oFile,oReader);
                        };
                        oReader.readAsDataURL(oFile);
                    }
                };
            },
            onProgress:function(e){//进度更新
                var me = this;
                if (e.lengthComputable) {

                    document.getElementById('progress_percent').innerHTML = me.iPercentComplete.toString() + '%';
                    document.getElementById('progress').style.width = (me.iPercentComplete * 4).toString() + 'px';
                    document.getElementById('b_transfered').innerHTML = me.iBytesTransfered;
                    if (me.iPercentComplete == 100) {
                        var oUploadResponse = document.getElementById('upload_response');
                        oUploadResponse.innerHTML = '<h1>Please wait...processing</h1>';
                        oUploadResponse.style.display = 'block';
                    }
                    document.getElementById('speed').innerHTML = me.iSpeed;
                    document.getElementById('remaining').innerHTML = '| ' + Library.fileUploader.secondsToTime(me.secondsRemaining);
                } else {
                    document.getElementById('progress').innerHTML = 'unable to compute';
                }
            },
            onError:function(e){//出错
                document.getElementById('error2').style.display = 'block';
            },
            onAbort:function(e){//中断
                document.getElementById('abort').style.display = 'block';
            },
        });

        var save_user_info = function(){
            var user = {
                name:document.getElementById("user_save_name").value,
                phone:document.getElementById("user_save_phone").value,
                photo:document.getElementById("hidden_file").value
            };
            session.pubuser.add(user);
            session.pubuser.save();
        };
        document.getElementById("startupload").addEventListener("click",function(){
            myUploader.startUpload();
            return false;
        });
        document.getElementById("user_save_button").addEventListener("click",function(){

            if (document.getElementById("hidden_file").value == "") {//未上传图片
                if (document.getElementById("myfile1").value == "") {//未选择图片
                    alert("your photo cant't be empty ");
                    return false;
                }else{
                    myUploader.onSuccess = function(e){//on success
                        //fileobj contains name,link,size
                        var oUploadResponse = document.getElementById('upload_response');
                        oUploadResponse.innerHTML = e.target.responseText;
                        oUploadResponse.style.display = 'block';
                        document.getElementById('hidden_file').value = e.target.responseText;//用于用户使用/保存
                        //触发保存
                        save_user_info();
                    };
                    myUploader.startUpload();//先上传图片,等待返回
                    return false;
                }
            }
            save_user_info();
            return false;
        });

    };
});

配置app/server_config/tmp_dir.js

设置上传文件存放的临时目录,默认为“tmp”,如果您想修改,可在该文件中配置

package.js

package.js用于将文件之间的依赖关系添加到Clouda中,我们可以使用下面的语法编写该文件:

 sumeru.packages(
    'student.js',
    .....

    'studentList.js'
 )

并不是在所有文件夹下新建文件或者文件夹后就要修改package.js文件,view文件夹和publish文件夹例外。

引入css文件

如果您有CSS文件需要引入到工程中,建议在app/package.js文件中添加需要的CSS文件,方法如下:

例如studentList.css放在app/asset/css目录下

 sumeru.packages(
    'student.js',
    .....

    'app/asset/css/studentList.css'
 )

在package.js中不仅可以引入CSS文件,当您需要引入jquery、backbone等第三方的库文件时,也可以在package.js中添加。

多单元集群支持

单机上开启多单元集群支持

修改server_config/cluster.js文件,添加相应的参数:

  sumeru.config.cluster({
    enable : true,
    host : 'your_redis_ip',
    port : 6379,
    dbname : 'your_redis_dbname',
    user: 'your_redis_user',
    password: 'your_redis_password',
  });

如果本机没有redis,请先安装redis并创建db。

BAE上开启多单元集群

在BAE上开启多单元的配置请点击这里