date: 2019-01-26 11:28:07
updated: 2019-01-26 11:28:07
Qlik Sense学习笔记之Mashup开发(二)
1.Mobile SPA UI Framework
-
Bootstrap
-
Onsen UI 推荐
可自动侦测IOS和Andriod等设备,自动适应设备的不同相应模式
-
Leonardo-UI
基于 Qlik Sense 自身的一个UI,不需要引入相关的 js、css 文件,只需要保证使用的类是一致的,就可以得到和 Qlik Sense 一样的UI样式,主要是用来开发 Extensions
-
jQuery Mobile
-
Material-UI
2.实战
2.1 从零到一
2.1.1 建立 Mashup Project
方法一:
本地Qlik Desktop 中打开Dev Hub -> Mashup Editor -> Basic Template,之后在我的电脑 -> 文档 -> Qlik -> Extensions 中可以找到刚才创建的模板,然后复制整个文件夹到WebStorm或IDEA的workplace下打开。
方法二:
打开Dev Hub -> Mashup Editor -> Basic Template,之后在服务器端下载刚刚创建的模板,会自动打包成zip文件,然后复制整个文件夹到WebStorm或IDEA的workplace下打开。
2.1.2 配置数据
-
安装sense-go
https://github.com/stefanwalther/sense-go
cnpm install -g sense-go
使用 -g 设置sense-go到全局变量 只需安装这一次报错:
F:\IDEA Workspace\mxxct_demo>npm install -g sense-go
npm ERR! code 1
npm ERR! Command failed: F:\Git\cmd\git.EXE checkout 4.0
npm ERR! error: pathspec '4.0' did not match any file(s) known to git
npm ERR! npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\Lenovo\AppData\Roaming\npm-cache\_logs\2019-01-17T13_39_08_763Z-debug.log更新安装方式:
在 C 盘下新建一个文件夹,命名为 patch,并将 sense-go.zip 复制到该文件夹下,
解压缩到 sense-go 文件夹,执行命名 npm install sense-go -g C:\patch\sense-go -
新建配置文件
打开上面网址,将 sense-go/lib/default-config.yml 中的内容复制下来,在自己项目的根目录下新建一个 yml 文件,文件命名为 .sense-go.yml (注意最前面有一个点),并将内容拷贝进去。
-
修改项目目录结构
sense-go官方文档推荐的项目结构如下:
| PROJECT-ROOT
|-- build <= all builds, including source code or zipped files
|-- dev <= target for the development build
|-- release <= target for the release build
|-- docs <= documentation files, then used by verb
|-- src <= all source files
|-- lib
|-- css <= see below *
|-- less <= less files
| .sense-go.yml <= sense-go configuration file (OPTIONAL)
| .verb.md <= verbs readme template
| package.json build文件夹会自动生成,不需要关心
docs文件夹下的文件可以自己管理一下,但是 sense-go 本身不会过多的关心这个目录下的内容
src文件夹存放的是所有的源代码,需要自己创建 lib、css等文件夹 -
配置 yml 文件
修改 packgeName,值为项目名称
可以添加一下 version 信息
-
修改 deployment -> toLocal -> enabled 为true,这样就可以使用本地所创建的APP,并把本地 Extensions 路径复制到 localExtensionsBaseDir 下
deployment:
toLocal:
enabled: true
pathFetching: true # By default the path will be automatically fetched. localExtensionsBaseDir: "C:/Users/Lenovo/Documents/Qlik/Sense/Extensions"
注意,要把路径中的 '\' 换成 '/' !!!
# defaults to the local extension directory of Qlik Sense,
# if pathFetching is enabled, this path will be determined automatically extensionDirOverride: # Define the extensionDir if you want to deploy to another directory than defined in packageName
qrs: # Not implemented, yet
enabled: false
url: null
toSsh: # Upload via SSH
enabled: false
host: "192.168.56.11"
port: 22
username: "usr"
password: "foobar"
dest: "/c/Users/usr/Documents/Qlik/Sense/Extensions/whatever-extension"
viaShell:
enabled: false qrs:在项目发布的时候会同步推到Qlik Sense服务器上,尽量不要将 enabled 置为true,可能会导致服务器压力过大
-
修改 lessReduce 中的配置
lessReduce:
# src: "./src/lib/less/main.less"
src: "./src/css/less/main.less"
# 这里 main.less 和项目中的文件名必须一致,要不然会找不到文件导致生成的 build 目录下没有 css 样式文件
# dest: "./.tmp/lib/css"
dest: "./.tmp/css"
-
添加 main.less 和 qlik.less 文件在 src/lib/css/less 文件夹下
less 文件夹如果不需要是可以不去配置的,不过因为 less 文件夹会在 build 过程中经过一次编译,并且可以设置一些常用变量,所以配置一下有利于开发。
在 main.less 中添加以下代码,引入 qlik.less文件
@import "qlik";
并且将 mxxct_demo.css 文件中的内容拷贝到 qlik.less 文件中,其中的内容是一些 qlik 默认模板的一些样式设置,然后可以将 mxxct_demo.css 文件删除。
删除 wbfolder.wbl 文件,其保存的是当前项目中的文件列表,不过是默认的文件列表,所以并没有什么用
修改 mxxct_demo.html 中对 mxxct_demo.js 和 mxxct_demo.css的引用路径,其中 mxxct_demo.css 需要改成 main.less,同时 css 文件夹只有在 build 的时候才会生成,所以这里的路径应该修改为生成 build 之后的文件路径
输入命令 sense-go build 来自动生成项目,生成的 build 文件夹会出现在本地 Qlik/Extensions/mxxct_demo 文件夹下,同时会同步在 mashup 当中,可以在 mashup 中点击预览查看,也可以输入 localhost:8848/extensions/mxxct_demo/mxxct_demo.html 进行预览
输入命令 sense-go watch:build 来自动监测 .sense-go.yml 从而自动生成 build 文件夹
2.1.3 Hello Qlik
-
在 mxxct_demo.js 文件中,引入 APP
var app = qlik.openApp("XXX.qvf",config)
app.getObject("div的id,比如QV01","object的id");
如果需要调用两个服务器上的APP,那么就需要配置两个 config,以及两个 require.config,一般来说是不太需要这种情况的。
XXX 指的是本地 Qlik/Sense/Apps 下的文件名称,是从QMC中export出来的文件名(仅限于本地运行)
当放到服务器上的时候需要将 XXX.qvf 换成服务器上打开 App 时 url 后的字符串,不带qvf,qvf指的是本地运行的文件。
所以尽量写一个 json 文件或者配置文件来配置变量,在上线的时候只需要修改配置即可 查看官方API插入到 mxxct_demo.js 文件中来使用
2.1.4 加入类库
-
第一种方法是静态加载:直接在 mxxct_demo.html 文件中添加,以OnsenUI为例,将对应的js、css文件放到 lib 文件夹下
<script src="lib/onsen.ui.js"></script>
-
第二种方法是动态加载:首先在 lib 文件夹下创建一个 mxxct_lib.js 文件,然后在 mxxct_demo.js 文件夹中的 require 函数那里,添加在 "js/qlik" 之后,逗号隔开,并且在后面的 function 那里写出对应的引用变量名称。要注意的是,由于 require 已经修改了自己的一个 baseUrl 地址,所以在引用的时候需要使用绝对路径而不能是相对路径,不然会找到文件。
var prefix = window.location.pathname.substr( 0, window.location.pathname.toLowerCase().lastIndexOf( "/extensions" ) + 1 );
var config = {
host: window.location.hostname,
prefix: prefix,
port: window.location.port,
isSecure: window.location.protocol === "https:"
}; // 在这里默认会把 require.js 中的 baseUrl 给修改掉,所以下面需要改成绝对路径
require.config( {
baseUrl: ( config.isSecure ? "https://" : "http://" ) + config.host + (config.port ? ":" + config.port : "") + config.prefix + "resources"
} ); // 在 require 中添加引用是不需要在文件后添加 .js 的
// 将 require 调整成如下格式,方便查看
require( [
"js/qlik",
"../extensions/mxxct_demo/lib/mxxct_lib"
], function (
qlik,
mxxctLib, // 这里设置的引用名称是不支持带 '-' '_' 之类的
) {
qlik.setOnError( function ( error ) {
$( '#popupText' ).append( error.message + "<br>" );
$( '#popup' ).fadeIn( 1000 );
} );
$( "#closePopup" ).click( function () {
$( '#popup' ).hide();
} ); //callbacks -- inserted here --
//open apps -- inserted here --
//get objects -- inserted here --
//create cubes and lists -- inserted here --
} );每引用一个文件,就需要写一次绝对路径,会略显麻烦,所以可以通过以下方法:引用某一个主 lib.js 文件,然后通过这个文件去引用其他的 js 文件。
在 mxxct_lib.js 文件中添加如下代码
alert("调用mxxct_lib.js文件成功"); // 这里的 define 函数类似于 mxxct_demo.js 文件中的 require 函数,需要一个数组存放调用的依赖,再加上一个回调函数
define([
"./sub_lib1", // 因为 sub_lib1.js 相对于 mxxct_lib.js 文件是在同一级文件夹下,所以不需要再写绝对路径,通过 ./ 写一个相对路径(当前文件夹下的 sub_lib1.js文件)即可,注意不需要写出来最后的 .js,不然会找不到文件
],function(
subLib1
){
subLib1.whoAmI();
});在 sub_lib1.js 文件中添加如下代码
define([],function(){
return{
whoAmI:function () {
alert("mxxct");
console.log("调用sub_lib1.js文件")
}
}
}); 保证类名一样的情况下,使用 Leonnardo UI 可以直接获得和 Qlik Sense 中一样的样式
2.2 渐入佳境
首先需要获取到对应的 APP 的appid
打开 Qlik Desktop -> Dev Hub -> 修改 url 中的 dev-hub 为 hub -> 打开 app -> 打开其中一个子 app -> 在 url 的最后添加 /options/developer -> 右键 object 选择 Developer,可以直接查看到 objectID
2.2.1 动态切换 Object
在获取 Object 的时候,必须要有高度,尽量不要用 min-height,没有高度会不显示,但是可以没有宽度,但是如果要保证换行效果的话,需要加上宽度限制,不然切换到手机屏幕的时候宽度会根据图表的宽度来自动匹配,有可能会导致失去图表效果
-
getObject() 的呈现效果和 visualization.get() 的呈现效果是一样的,都是获取 object 并且呈现,但是 visualization.get() 如果不 show 的话,vis 会一直存在,并且 vis 保存的是整个 object 的一个 datamodal 形式,可以查看到 object 的相关信息,比如图表的类型,组合模式,标题等等,在 layout -- qHyperCube 下,可以查看 object 的数据内容,可以单独拿出这部分的数据来单独呈现或生成表格等等。
var app = qlik.openApp('xxxName.qvf',config);
app.getObject('QV01', 'TT'); var app = qlik.openApp('xxxName.qvf',config);
app.visualization.get('TT').then(function(vis){
vis.show("QV01");
}); 动态切换
方式一:
button 后面添加函数:onclick="window.fn.getFirst()",需要把函数写到静态中,然后在主JS文件中写入函数体。
方式二:
通过 jQuery 来绑定 button 的点击事件
var app = qlik.openApp('xxxName.qvf',config);
var chart = [];
var promises = [];
promises.push(app.visualization.get('TT').then(function(vis){
// vis.show("QV01");
// 返回 promise 的处理结果,但是不知道处理结果是成功还是失败
return Promise.resolve(chart.push(vis));
}));
promises.push(app.visualization.get('EJpVXek').then(function(vis){
// vis.show("QV02");
return Promise.resolve(chart.push(vis));
}));
$("#btn1").on("click",function () {
$("#btn1").addClass("lui-button--info");
$("#btn2").removeClass("lui-button--info");
chart[0].show("QV01");
});
$("#btn2").on("click",function () {
$("#btn1").removeClass("lui-button--info");
$("#btn2").addClass("lui-button--info");
chart[1].show("QV01");
});
// 先把所有的 button 都置为不可选
// 当且仅当 promises 中的所有处理结果都存放进来了,即所有获取数据的函数都已经走完了,再执行以下函数
Promise.all(promises).then(function () {
chart[0].show("QV01");
$("#btn1").removeAttr("disabled");
$("#btn2").removeAttr("disabled");
});
// 需要注意的是,如果在获取 'TT'、'EJpVXek' 之间也存在因为数据量而产生先后顺序错乱的问题,可以固定数组下标来保证先后顺序一致,推荐修改为下面的形式:
var app = qlik.openApp('xxxName.qvf',config);
var chart = [];
var promises = [];
promises.push(app.visualization.get('TT').then(function(vis){
// vis.show("QV01");
// 返回 promise 的处理结果,但是不知道处理结果是成功还是失败
return Promise.resolve(vis);
}));
promises.push(app.visualization.get('EJpVXek').then(function(vis){
// vis.show("QV02");
return Promise.resolve(vis);
}));
// 先把所有的 button 都置为不可选
// 当且仅当 promises 中的所有处理结果都存放进来了,即所有获取数据的函数都已经走完了,再执行以下函数
// 变量 chart 里面的内容可能是异步的,但是通过 Promise.all(promises) 函数对 promises 会再做一次同步操作,会将 chart 的顺序和 promises 的顺序保持一致
Promise.all(promises).then(function (chart) {
chart[0].show("QV01");
$("#btn1").on("click",function () {
$("#btn1").addClass("lui-button--info");
$("#btn2").removeClass("lui-button--info");
chart[0].show("QV01");
});
$("#btn2").on("click",function () {
$("#btn1").removeClass("lui-button--info");
$("#btn2").addClass("lui-button--info");
chart[1].show("QV01");
});
$("#btn1").removeAttr("disabled");
$("#btn2").removeAttr("disabled");
});
2.2.2 Selection 与 Clear
在筛选项中有一个比较有意思的点,就是可以通过 API 来设置某几个图表或者筛选项的 Alternate State,通过这个 State 的值可以将某些图表和筛选项固定住,比如原先所有的数据都会造成整个 APP 的数据联动,现在要看1月份和2月份的一个饼图对比,在设置了 State 的值之后,这两个饼图的数据不会联动,只会各自根据筛选项进行联动,在保证饼图的算法是一致的情况下,可以获得对照的一个效果。
clearAll() 是对整个 APP 来清楚筛选项,所以是 app.clearAll(),Selection 是对某个 Filed 来进行选择,所以是 app.field("name").select([0, 1, 2], true, true)
-
qlik.app.field.select(Array, toggle, softlock)
toggle:true 表示之前做的选择会累加不会被清除掉,false 表示之前做的选择全部清除只保留本次的选择,选择 true 的时候如果一个选择 button 点两次会恢复到初始状态,需要根据情况来决定
softlock:如果是 true,之前锁住的选择会被覆写掉
-
查看要选择的 field 中的数据
var field = app.field('实体公司');
field.getData();
console.log(field); -
选择最大值
$("#btn6").on("click",function () {
// app.field("实体公司").select([0], true, true);
var field = app.field('实体公司');
field.getData();
// console.log(field);
// 因为获取数据的问题,刚点击的时候,field.getDdata() 还没获取到数据的时候就已经进行选择了,为了避免这个问题就要加一个延时
setTimeout(function () {
var res = getMaxValueIndex(field.rows);
app.field("实体公司").selectValues(res, false, true);
},500); //field.getData();
//after data is loaded fieldvalues will be in rows array
//field.rows[0].select();
}); function getMaxValueIndex(paramArray) {
console.log(paramArray); var names = [];
var tmpMaxValue = Number.MIN_VALUE;
for(var i = 0; i < paramArray.length; i++){
if(Number(paramArray[i].qFrequency) > tmpMaxValue){
tmpMaxValue = Number(paramArray[i].qFrequency);
names = [];
names.push(paramArray[i].qText);
}else if(Number(paramArray[i].qFrequency) == tmpMaxValue){
names.push(paramArray[i].qText);
}
}
console.log(names);
return names;
}
2.2.3 变量控制
Qlik 中数据大多以 ( num,string ) 的方式存在,通过 app.variable.setStringValue(qName, qVal) 和 app.variable.setNumValue(qName, qVal) 这两个方法可以对任意一端进行赋值,Qlik 会自动对另一端的值进行改动,不需要再去手动修改
-
当 APP 中同一个 objectID 被三个 button 复用来进行筛选,比如 "30天内"、"90天内"、"全部",那么就可以通过 app.variable.setStringValue("qName","qValue") 来进行筛选
$("#btn7").on("click",function () {
app.variable.setStringValue("vTimeRanges","in30days");
$("#btn7").addClass("lui-button--info");
$("#btn8").removeClass("lui-button--info");
$("#btn9").removeClass("lui-button--info");
}); -
获取变量中的值,通过 app.variable.getContent() 来获取,并且需要有一个异步函数调用的过程,来对获取到的值进行处理
app.variable.getContent('MYVAR',function ( reply ) {
alert( JSON.stringify( reply ) );
} );
2.3 深入浅出
2.3.1 Hypercube
Qlik 中所有的数据存储方式都是 qHyperCube,不再是数据表中的二维形式。
表格的数据是动态加载的,当用户在向下滑动表格的时候,会不断去获取数据动态加载到表格中,其他的 object 是静态加载到前台页面的。
-
通过
app.visualization.get("id").then(function(vis){
console.log(vis);
});来输出 dataModal,在 modal --> layout --> qHyperCube --> qDataPages --> qMatrix 下可以找到当前 object 的所有数据
通过 qHyperCube --> qGrandTotalRow 可以得到这个 object 某个维度的总数
通过 qHyperCube --> qDimensionInfo 可以得到这个 object 某个维度的描述和字段值
如果只是从 qHyperCube 中获取到某些值,展示出来的值是静态的,需要再添加数据联动的效果。
2.3.2 数据联动
通过 vis.table.OnData.bind(listener) 方法来把 vis 的值绑定到 object 上去,但是会遇到一个问题,就是如果这个 object 之前没有被 show 过的话,是无法联动的,所以需要在 .html 中添加一个 style="display:none" 的一个 div,用来存放需要被联动的 kpi
-
具体代码 copy from demo.js
// 在 demo.js 中先添加下面的赋值函数
function renderKpi(kpi) {
$("#QV02").text(kpi.model.layout.qHyperCube.qDataPages[0].qMatrix[0][0].qText);
$("#QV03").text(kpi.model.layout.qHyperCube.qDataPages[0].qMatrix[0][1].qText);
$("#QV05").text(kpi.model.layout.title).css("background-color", kpi.model.layout.subtitle);
} // kpi
// 在获取到 kpi 之后,将 vis 的值 render 到 object
promises.push(app.visualization.get('UtxwSV').then(function (vis) {
console.log(vis); var listener = function () {
renderKpi(vis); // 分页的时候,避免内存溢出将 vis 关闭并且解绑 listener
// vis.close();
// vis.table.OnData.unbind(listener);
}; vis.table.OnData.bind(listener);
return Promise.resolve(vis);
})); // 需要在 demo.html 中添加一个 style="display:none" 的 div,为了让 object 先 show 一下,但是需要隐藏掉,因为不是为了查看某个 object
<div id="cachePool" style="display:none">
<div id="QV04"></div>
</div> // 在 demo.js 中的 Promise.all() 函数中添加以下部分
Promise.all(promises).then(function (charts) { // 将 object 先 show 一下
charts[2].show("QV04"); // 在第一次获取到数据之后把 kpi 放到 div 中
var kpi = charts[2];
renderKpi(kpi); // Selection 的侦听
var selState = app.selectionState();
var listener = function () {
if (selState.selections.length > 0) {
$("#QV06").text(selState.selections[0].qSelected);
} };
selState.OnData.bind(listener);
});如果联动特别多,那么每一个 visualization 都需要绑定一个 listener,所以最好把需要联动的数据集中在一个 object 中,一次性获取到所有的需要联动数据
2.3.3 Selection的侦听
- 查看上方代码,需要注意的是,在 chart 上一次性最多选择6个,大于6个就会显示 n of totalNum(n > 6),所以需要显示更多的选项,就需要去添加一个筛选下拉列表,然后侦听筛选器的选中状态
2.4 更上层楼
2.4.1 Object 预读
- 预读其实就相当于把 object 先放到了 promise 里面,当所有的 object 都已经放到 promise 中之后,再
2.4.2 Config JSON
-
把所有的静态变量写在一个 config 文件中,然后在 js 文件中通过 ajax 来调用
$.ajax({
'type': 'get',
'url': "config/system.json",
}).done(function (reply) {
...
})
2.4.3 SPA 的实现
- 如果是多个页面进行跳转的话,server 端需要不断提供新的链接信息,socket、session等等,所以 Qlik 推荐使用 SPA 的方式来实现 mashup,可以通过对 div 的 show / hide 或者通过框架对页面重新渲染等
2.5 登峰造极
2.5.1 Websocket 与 Engine API
- 辅助工具 socket.io