本文希望通过解决以下疑问,从根本上告知大家如何调试的问题:
1,smartbi是如何加载js、css文件的?
2,是如何实现与服务端异步或同步沟通的?
3,在smartbi添加一个拥有复杂界面(有交互事件)的弹窗如何实现?
4,面对异步请求、鼠标事件等存在多浏览器问题,smartbi是如何处理的,有没有提供一些工具类方法
5,smartbi里面的每个交互界面,是如何衔接在一起的,每个界面是个独立的jsp还是通过js动态衔接的?
1、Smartbi前端框架
1.1、前端组件框架
是典型的基于JavaScript的面向对象框架,整个系统只有几个入口jsp(譬如index.jsp、login.jsp),剩下基于AJAX按业务或操作逻辑按需动态加载或注销组件,譬如在系统中双击一张灵活分析报表,系统就会调出灵活报表组件(QueryView)并执行打开的操作。下图是简单的前端组件图,对于定制来说最常做的操作是编写或修改业务逻辑层的组件。
上图分为四层:
底层组件:是工具类、抽象接口类性质,下面章节会重点介绍几个常用的
基础控件:类似下拉框、表格、树、tab、列表、弹窗等界面控件或对象
业务控件:基于基础控件又封装的一层具有业务意义的控件,譬如定制管理左侧的资源树就是资源树控件
业务逻辑:整个系统只有几个入口jsp,并不是说所有内容一次性加载,而是根据用户鼠标操作按需加载或注销内容,所以系统的每个功能,其实都会对应一个js组件,譬如灵活报表、电子表格、透视分析、多维分析等都会有自己对应的组件,通过定制给系统增加一个功能界面也相当于要创建一个业务逻辑组件。
1.2、前后端通信框架
这里分两类介绍前后端通信,文件类交互请求,譬如js、html、css,和操作或数据交互类请求,譬如刷新报表,新建报表之类。
JS文件: 使用jsloader方式按需加载js文件,jsloader也是封装了请求gbk.jsp?name=jsname,见替换Smartbi文件里的说明;
CSS文件:为了减少css的请求,系统采用bof_merge.css.jsp将css合并一次性加载,同时提供了扩展点修改系统内的样式或新增样式,扩展点中配置的css文件,bof_merge.css.jsp会自动识别并加载;
HTML文件:html文件一般作为组件的布局和展现文件,下面介绍的2.2 Module2:js组件基类如果使用了基类的init方法,默认会自动加载同名的.html文件,也可以使用domutils.doGet("相对于vision的html完整路径")方式加载指定的.html或.template文件,例如domutils.doGet("template/freequery/query/QueryView.template");
操作和数据交互:使用util.remoteInvokeEx/remoteInvoke与服务端module交互,如果是希望与jsp/servlet交互,可以使用domutils中提供的doPost/doGet方法交互。
2、关键组件介绍
2.1、JSLoader:提供加载js的方法
smartbi内置了一个全局对象jsloader, 就是通过这个脚本对象提供的几个方法异步加载js,如果很多地方重复加载一个js,系统会自动缓存,只会从服务器请求一次。
jsloader本质是使用gbk.jsp加载要能让jsloader正常加载到js,需要遵循:
1)所有js文件必须置于“vision/js/”目录或其子目录下
2)js文件名(不含后缀)及其所有父目录的文件名都不能包括“.”
3) 模块内必须定义一个与文件名同名的全局变量
jsloader中三个加载js方法说明见下文:
resolve(name, useGlobal):按需加载并执行
参数说明:
name:要加载的js名称,是相对于vision/js目录的完整路径名,譬如加载vision/js/freequery/lang/CustomEvent,就是"freequery.lang.CustomEvent"
useGlobal:是否全局,缺省为false,如果为true,就等同于使用<script>标签加载resolve示例//全局加载,等同于在index.jsp中使用<script>标签引用,被加载脚本中的全局对象可以直接使用 jsloader.resolve('thirdparty.jquery.jquery', true); //局部加载,等同于局部变量,另外一些不能引用这个变量的地方需要的话又得重新resolve var util=jsloader.resolve("freequery.common.util"); var CustomEvent = jsloader.resolve("freequery.lang.CustomEvent"); //返回的只是freequery.lang.CustomEvent类,还没有实例化
resolveMany(names):批量加载但不执行 ,意思是批量从服务端请求js,减少与服务端沟通时间
参数说明
names:要加载的js名称数组,是相对于vision/js目录的完整路径名,如['freequery.common.util','bof.baseajax.common.Application']resolveMany示例jsloader.resolveMany(['freequery.common.util','bof.baseajax.common.Application','freequery.widget.Module' ]); //后面如果再想使用具体的类时: var util=jsloader.resolve("freequery.common.util"); //这个时候实际不会发起服务端请求的,只是会从缓存里面拿到脚本并eval执行
imports(className):仅声明待用到再加载与执行
参数说明
className:要加载的js名称,是相对于vision/js目录的完整路径名,譬如:"freequery.lang.CustomEvent"imports示例var util = jsloader.imports("freequery.common.util"); //用的使用需要调用getInstance()去实例化对应的对象,例如: util.getInstance().remoteInvokeEx(...)
2.2 Module2:js组件基类
完整名称:freequery.widget.Module2,可以是所有业务逻辑组件的基类,一般情况,编写界面是同名js文件和html文件配套,前者是组件业务逻辑,一般会继承freequery.widget.Module2,后者是布局文件,Module2内置了如下逻辑:
1)使用其中的addListener和removeListener方法给dom元素注册事件
2)使用其中的init方法,实现了界面逻辑和界面布局的分离,界面布局可以是与组件js文件同目录及同名的.html,这样系统会自动加载布局文件,同时会给布局html文件中定义了bofid的元素自动执行以下操作
- 定义了bofid的dom元素,可以在js组件中通过this.elem+bofid,其中首字母大写引用,譬如:<span bofid=“testSpan” />,则this.elemTestSpan可以引用该元素
- js组件可以使用以下方法给元素添加事件:elem + bofid,其中首字母大写 + 事件名称+_handler,如果在组件中添加这类规则的方法,系统会自动给对应元素加上对应鼠标事件,譬如elemTestSpan_click_handler,就是给bofid为testSpan的元素添加click事件
3)destroy方法,注销组件
完整的示例见下面的
/** * 为一个DOM对象添加事件, 供子类调用 * 示例:this.addListener(this.elem_btnQuery , "click", this.refreshData , this); * @modifier final, protected * @param element * 一个DOM对象 * @param type * 待绑定的事件类型, 如'click', 'mouseup'. 注意: 不要加'on'前缀 * @param handler * 处理函数 * @param that * [可选] 处理函数中this所指的对象 * @param group * [可选] 很少用到. 用于对"事件绑定"分组, 便于按组解除绑定. 见removeListenersByGroup() * @return void */ Module2.prototype.addListener = function(element, type, handler, that, group) {} /** * 解除对一个DOM对象的事件绑定, 供子类调用 * 示例:this.removeListener(this.elem_btnQuery,"click",this.refreshData); * @modifier final, protected * @param element * 一个DOM对象 * @param type * 待绑定的事件类型, 如'click', 'mouseup'. 注意: 不要加'on'前缀 * @param handler * 处理函数 * @param that * [可选] 处理函数中this所指的对象 * @param group * [可选] 绑定时给出的组名. 见addListener() * @return void */ Module.prototype.removeListener = function(element, type, handler, that, group) {} /** * @param container 父容器dom对象 * @param url html模板路径或内容,传递__url 代表与当前js组件同目录同名的html文件 * @param noWrapper 是否创建一个父容器,true or false,缺省是false, 意思是直接用第一个参数container作为第二个参数html内容的父容器,如果为true,会再创建一个父容器包装第二个参数HTML,最后再赋给第一个参数container * @param noDoGet 这个是说是否需要请求html内容, true代表不用请求,也就是第二个参数传递的是html内容,false代表需要,就是第二个参数传递的是html模板路径 */ Module2.prototype.init = function(container, url, noWrapper, noDoGet) {}
2.3、domutils:工具类
doGet(url,notUseGBKJSP) :使用get方式请求url
参数说明:
url:要请求的url地址,如果是系统内部资源,是相对于vision的url,譬如:template/freequery/query/QueryView.template
notUseGBKJSP:是否使用gbk.jsp加载,默认为false,gbk.jsp是系统用于加载js,.template,.html文件的一个jspdoGet示例var template = domutils.doGet("template/freequery/query/QueryView.template"); this.panel = document.createElement("div"); this.panel.innerHTML = template;
doPost(url, data, callback, errorHandler, scope, headers):使用post方式请求url
参数说明:
url:如果是系统资源,相对于vision地址的url
data:post的数据,譬如:"A=xx&B=yy"
callback:请求成功返回的回调函数,如果传递了此方法就是异步请求,否则同步请求
errorHandler:请求异常的回调函数,只有传递了callback参数时,此参数才生效
scope:callback函数内部的this对象
headers:请求头信息,json对象,譬如{If-Modified-Since:0}doPost示例片段//以下示例示意说明,并不能真的运行 var url = "RMIServlet"; var data = null; data ="className=" + encodeURIComponent(className) + "&methodName="+encodeURIComponent(methodName) + "¶ms="+encodeURIComponent(paramsStr); domutils.doPost(url, data, function(responseText) { var export2FtpUtil = jsloader.resolve("aladdin.utils.Export2FtpUtil"); export2FtpUtil.showExportResult(responseText); }, function(xhr) { alert("导出失败!"); }, this, null);
addClassName/removeClassName/hasClassName(elem,value):给dom元素添加或删除css样式类
参数说明:
elem:dom元素对象
value:样式类名if (domutils.hasClassName(elem, 'awesomplete')) { domutils.addClassName(elem, 'search-wrapper'); //domutils.removeClassName(elem, 'search-wrapper'); }
- isIE():是否是IE
is+浏览器英文名代表判断是否xx浏览器的方法,例如isFirefox、isIE、isIE6、isIE11、isEdge、isChrome、isQQBrowser、isSafari、isOpera、isIE7、isIOS 、isAndroid、isMobile。
2.4 util:工具类
remoteInvokeEx /remoteInvoke(className, methodName, paramArray, callback, that, headers)
与服务端module沟通方法,其中remoteInvokeEx如果同步请求出现异常会自动弹窗提示,参数说明:
className:配置再applicationContext.xml中注册到rmi中的名称,譬如下面示例中就是ExtSample8Service
methodName:要请求module中的哪个方法
paramArray:上面方法接收的参数数组,数组中的第一个对应方法的第一个参数,依次类推
callback:回调函数,请求返回执行,如果不传递此参数代表同步请求
that:callback里的this对象
headers:请求头信息,譬如:json对象,譬如{If-Modified-Since:0}可执行示例请见宏代码中执行sql语句。
module调用示例//以下示例只是说明用法,实际缺少很多上下文环境,并不能运行 //同步请求方式 var ret = util.remoteInvoke("DashboardService", "getParamValueFromDashboard", [this.clientId, paramId]); if(ret.succeeded) { return ret.result; } else { modalWindow.showServerError(ret); } //异步请求方式 var ret = util.remoteInvoke("DashboardService", "getParamValueFromDashboard", [this.clientId, paramId], function(ret){ if(ret.succeeded){ var result = ret.result; //getParamValueFromDashboard方法返回的结果,如果是服务端返回的是对象,这个就是个json对象 } }, this);
- getCookie(name)
获取指定名称cookie值。
- getSystemConfig(key)
获取指定key的系统选项值
2.5、lang:工具类
extend (subclass, superclass) :类的继承
- patch (subclass, superclass)
提供重写js类的构造方法,请见如何修改Smartbi JS文件。
- parseJSON (jsonString)
toJSONString(obj)
2.6、CustomEvent
var util = jsloader.resolve('freequery.common.util'); var CustomEvent = resolve("freequery.lang.CustomEvent"); var SpreadsheetReport = function(container) { SpreadsheetReport.superclass.constructor.call(this, container); //组件中定义事件方法 this.onAfterRefresh = new CustomEvent("AfterRefresh", this); //触发事件方法: //this.onAfterRefresh.fire(this); //其中参数可以任意多个 //注册事件方法 //this.onAfterRefresh.subscribe(this.doParamChangeRefresh, this); //取消注册事件 //this.onAfterRefresh.unsubscribe(this.doParamChangeRefresh, this); } lang.extend(SpreadsheetReport, "freequery.widget.Module2"); SpreadsheetReport.prototype.doParamChangeRefresh = function() { this.onAfterRefresh.unsubscribe(this.doParamChangeRefresh, this); this.doParamChangeNeedRefresh = false; var that = this; setTimeout(function() { //xx }, 1); }
2.7、BaseDialogEx
- init(parent,data,fn,obj,win):初始化方法
参数说明:
parent:窗口内容的父容器
data:调用弹窗时传递给弹窗的数据
- destroy():注销方法