Publish
Clouda使用PubSub模型描述数据的传输,其中,publish是发布数据的方法,其运行在Server上,每一个publish文件均需要放置在publish/。
- module.exports = function(sumeru){
- sumeru.publish(modelName, publishName, function(callback){
- }); }
可以看到在sumeru.publish()
中有三个参数,modelName
、publishName
和一个匿名方法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",
- //{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;
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将不同操作区分开来。
声明中:
-
deleteUrl
,insertUrl
,updateUrl
三个方法作用等同于postUrl
,返回不同操作下三方接口url信息; -
onDelete
,onInsert
,onUpdate
三个方法作用等同于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);
- 详细代码和说明请参考《Examples》文档中SpiderNews实例。
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说明
部分做详细的说明。 -
#p#
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
字段的数据类型,包括"int"、"datetime"、"string"、"object"、"array"、"model"、"collection"。
除以上两种,常用的属性还包括:
-
defaultValue
字段的默认值
{name: 'gender', type: 'string', defaultValue: 'male'}
若不提供"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');