Loading

Qlik Sense学习笔记之插件开发

date: 2019-05-06 13:18:45
updated: 2019-08-09 15:18:45

Qlik Sense学习笔记之插件开发

1.开发前的基础工作

1.1 新建插件

dev-hub -> Extension-Editor -> Create new project -> 自己起一个名字,在模板 template 下拉列表中选择 Angular Basic Visualization template,之后在 qmc 中下载新建的插件,并导入到 IDE 中编辑打开。

1.2 项目结构

新建的插件一共包含4个文件,以插件名称为 test 为例,四个文件分别是:test.js,test.qext, template.html, wbfolder.wbl。

  1. test.js:是主 js 文件,包括插件的一些自定义配置选项,以及获取数据的函数方法,处理数据一般会引用其他的 js 文件,调用其中的函数来处理,以避免文件过长显冗余。

  2. test.qext:是一个 json 格式的配置文件,name 是你在使用插件的时候所用到的名字,比如你的项目名称为 test,但是这里的 name 写的是 "name": "TEST",那么在打包上传到 qmc 之后,在 qmc 里显示的是 test,但是在具体使用的时候是插件的名称是 TEST。在 description 中可以填写具体插件的描述,如果需要添加自定义图片的话,类似于插件的封面图,可以添加 "preview":"test.png",照片可以放在项目的根目录下,直接引用即可,qlik sense 官网给出的图片大小建议是 140*140 png 格式。

  3. template.html:是插件内容呈现的页面,使用的语法是 AngularJS。

  4. wbfolder.wbl:凡是在这里面填写出来的文件都可以在 dev-hub 中被打开编辑,即如果你引用了类似 echarts.js 这种的文件,你只想使用但不需要编辑,那只需要在 test.js 最上面通过 require 的方式引用即可,无需再添加在 wbfoler.wbl 中。在 dev-hub 中点击某插件时,会最先访问该文件,如果不想让其他使用者在 dev-hub 中打开本插件,可以直接删掉,并不会影响正常使用。

1.3 代码结构

define( ["qlik", "text!./template.html"],
    function ( qlik, template ) {

        return {
            template: template,
            support: {
                snapshot: true,
                export: true,
                exportData: false
            },
            paint: function () {
                return qlik.Promise.resolve();
            },
            controller: ['$scope', function ( $scope ) {
                //add your rendering code here
                $scope.html = "Hello World";
            }]
        };

    } );

刚新建的插件只有这一部分最基础的代码,最上面是通过 require 的方式引入的 template.html,与编写 mashup 时引入文件是一样的。

support 里面需要配置的是 qlik sense 自带的功能,比如截图、导出数据,我们需要把 exportData 改为 true,这样就可以右键点击插件导出数据了。

插件第一次运行的时候会先执行 controller 里面的内容,之后会调用 paint 函数,同时每次修改(比如放大缩小插件所在的div)都会调用 paint 函数,以实现自适应或者数据的更新。

paint 函数与 controller 函数之间变量上的互通是通过 $scope 这一个参数来实现的。

2.具体开发

这里的话会按照 比如要引入文件、获取数据等具体的操作来进行分类说明

2.1 修改项目结构

按照 mashup 的编写习惯的话,我们会在项目根目录下新建文件夹,来保证项目结构不会因为文件太多而显得冗余。

原始项目结构
test
- test.js
- test.qext
- template.html
- wbfolder.wbl

修改后的项目结构
test
- lib
    - js
        - xxx.js
    - css
        - xxx.css
    - img
        - xxx.png
- test.js
- test.qext
- template.html
- wbfolder.wbl

在 lib 文件夹下存放了开发需要的各种 js、css、img 文件,只需要在 test.js 文件中引入即可。

2.2 引入 css 文件

假设 css 文件名称为 style.css,我们可以如下引用该文件:

define( ["qlik", "text!./template.html", "text!./lib/css/style.css"],
    function ( qlik, template, style ) {
        $("<style>").html(style).appendTo("head");
        return {
            template: template,
            support: {
                snapshot: true,
                export: true,
                exportData: false
            },
            paint: function () {
                return qlik.Promise.resolve();
            },
            controller: ['$scope', function ( $scope ) {
                //add your rendering code here
                $scope.html = "Hello World";
            }]
        };
    } );

主要是在第一行 define 中通过路径引入该 css 文件,同时在 function 函数中的第一行中将该 css 文件添加到 template.html 文件的头部,即可使用,需要注意的是在 define 中引入的文件,如果你在下面的代码中需要用到这个文件的话,就必须在 function 函数的形参中添加一个指代名称才行, qlik sense 会按照顺序去匹配,缺少形参指代会无法执行。这个形参可以用 '_' 下划线来代替。

"text!./lib/css/style.css" 中的 text! :只有当后面引用的文件是 js 文件的时候可以省略。

2.3 引用 jquery

类似于 mashup 引用,不需要添加什么 js 文件,直接引用即可。

define( ["jquery", "qlik", "text!./template.html", "text!./lib/css/style.css"],
    function ( $, qlik, template, style ) {
        $("<style>").html(style).appendTo("head");
        return {
            template: template,
            support: {
                snapshot: true,
                export: true,
                exportData: false
            },
            paint: function () {
                return qlik.Promise.resolve();
            },
            controller: ['$scope', function ( $scope ) {
                //add your rendering code here
                $scope.html = "Hello World";
            }]
        };
    } );

一般会把 jquery 放在第一个, 并习惯用 '$' 来指代。

2.4 引用 js 文件

与引用 css 文件不一样的地方是,require 会自动识别 js 文件,即在引用的时候是不需要注明文件拓展名的。以引用 render_radar.js 和 echarts-all.js 文件为例,前者是我们用来处理数据的一个子 js 文件,后者是 echarts 生成图表的官方 js 文件。

define( ["jquery", "qlik", "text!./template.html", "text!./lib/css/style.css", 
        "./lib/js/render_radar", "./lib/js/echarts-all"],
    function ( $, qlik, template, style, render_radar ) {
        $("<style>").html(style).appendTo("head");
        return {
            template: template,
            support: {
                snapshot: true,
                export: true,
                exportData: false
            },
            paint: function () {
                return qlik.Promise.resolve();
            },
            controller: ['$scope', function ( $scope ) {
                //add your rendering code here
                $scope.html = "Hello World";
            }]
        };
    } );

你会发现我们只对 render_radar.js 起了一个形参指代的别名,在 function 函数中我们可以通过 render_radar.xxx(p1, p2……) 的方式来调用 render_radar.js 中的函数,而 echarts-all.js 并没有别名指代,这是因为在生成图表的时候我们必须用到 echarts-all.js 这个文件,但是我们并不会对这个文件进行任何操作,那我们就不需要起一个别名指代(可以起但没必要)。

2.5 调用其他 js 文件中的方法

在上面我们已经调用了 render_radar.js 文件,在该 js 文件中我们定义以下函数方法:init_radar_chart,这样就可以在 test.js (主 js 文件)中通过 render_radar.init_radar_chart(param1, param2, param3, ……) 方法来调用该函数。

define([], function(){
    return ({
        init_radar_chart:function(param1, param2, param3, ……){
            . . .
        }
    });
});

这里需要注意的是,return 中的函数是外部文件引用本文件时可以调用的函数,如果需要调用本文件的函数可以通过以下方式:

define( [],
	function () {
        function func2() {
            console.log("2");
        };
        return {
            func1:function() {
				console.log("1");
				func2();
			}
        };
    });

同理,我们也可以在 render_radar.js 文件中引用其他的 js 文件(在 define 中引用 js 文件,在 function 中添加形参指代)以便调用其他文件中的函数方法。

2.6 调用图片文件

如果是在 qext 文件中引用一个图片来作为封面的话好说,可以把图片放到根目录下,直接调用。但是如果是引用一个图片来作为背景图,或者是在设置div的背景图的话,引用的会稍微不同,具体引用方式如下:

background: url("../extensions/test/background.png") no-repeat;

在插件使用图片的时候,qlik sense 会自动封装出来一个路径,如果是按照下面的方式写(前提是 css 和 img 这两个文件夹同级)

background: url("../img/background.png") no-repeat;

会报找不到图片的错误,通过查看错误原因,我们可以这样把访问图片的路径固定写成这样,test 是插件名称,这样就可以访问到图片。

2.7 添加维度、度量等相关自定义配置

与 template、support、paint、controller 平级,添加一下代码:

definition: {
    type: "items", // 表明这一级是由好几个item组成,即有子结构体
    component: "accordion",
    items: {
        dimensions: { // 维度
            uses: "dimensions",
            items:{
                title: {
                    type: "string",
                    label: "标题", 
                    ref: "qAttributeExpressions.0.qExpression", 
                    component: "expression",
                    defaultValue: ""
                },
            },
        },
        measures: { // 度量
            uses: "measures",
            items:{
                maxMeasure: {
                    type: "string",
                    label: "度量最大值",
                    ref: "qAttributeExpressions.0.qExpression",
                    component: "expression",
                    defaultValue: ""
                },
            },
        },
        sorting: {  
            uses: "sorting"
        },
        addons: { 
            uses: "addons",
            items: {
                dataHandling: {
                    uses: "dataHandling"
                }
            }
        },
        appearance:{ 
            uses: "settings"
        }
    }
},

Qlik Sense官方对自定义属性的介绍

下方链接:
https://help.qlik.com/en-US/sense-developer/June2019/Subsystems/Extensions/Content/Sense_Extensions/Howtos/custom-integer-properties.htm

Field Description
type Used for all custom property type definitions. Can be either string, integer, number, array or boolean.This field is mandatory and should always be "integer" for an integer property type definition.
component Used for defining how the property is visualized in the property panel. Used to override the default component that comes with the type setting.
label Used for defining the label that is displayed in the property panel.
ref Name or ID used to reference a property.
defaultValue Used for defining the default value of your custom property.
min Used for defining the minimum value of the property.
max Used for defining the maximum value of the property.

这段代码中,use 后面写出来的值都是 qlik sense 自己的相关默认配置,固定写法。

ref 中的值需要着重注意,需要分两种情况来看。

  1. 如果这是在维度和度量里的,ref 的填写方式就是上面这样,这是将“标题”中的数据存到了 qMatrix 中,即我们通过 backendApi.getData() 获取数据的时候会一并带回。同时,如果还想添加数据,ref: "qAttributeExpressions.0.qExpression", 只需要把 0 改成 1 即可,可以一直写。

  2. 如果是用户填写的一些自定义选项,比如字体大小、字体颜色等选项,我们可以选择下面的写法。

fontcolor : {
   ref: "fontcolor",
   label:"字体颜色",
   type: "string",
   expression: "optional",
   defaultValue: '#1E90FF'
},
fontsize: {
   type: "integer",
   label: "字体大小",
   ref: "fontsize",
   defaultValue: 24
}

其中 ref 里的值会直接保存在 $scope.layout 中,可以在 paint 中获取到,并实时进行更新。

2.8 通过 backendApi 来获取数据

paint: function (p1,p2) {
    let _this = this;
    var self = this, requestPage = [{
        qTop : 0,
        qLeft : 0,
        qWidth : 10,
        qHeight : 100
    }];
    // backendApi 只能在 paint 函数中使用
    this.backendApi.getData(requestPage).then(function(dataPages) {
        //_this.$scope.data = dataPages[0].qMatrix;
        var vData = dataPages[0].qMatrix;
        console.log(vData);
        
        // 动态获取当前object的高度和宽度,并重新赋值echarts图表,自适应
        var echart_id = "#" + _this.$scope.getId();
        
        var height = p1[0].offsetHeight;
        var width = p1[0].offsetWidth;

        
        $(echart_id).height(height);
        $(echart_id).width(width);
    });
    return qlik.Promise.resolve();
}

requestPage 中定义了我们要如何获取数据,因为 qlik sense 的数据都是存放在数组里的,所以我们可以定义从第几行第几列开始获取,以及获取几列几行数据。一次性获取到的总数量是2W,即 行 * 列 <= 2W,如果超过了这个数据量,可以循环去取。

你会发现我们在 paint 的函数里添加了两个参数:p1 和 p2。

  1. p1 指代的是整个 object 的信息,比如用户拖拉整个 object 的实时高度和宽度,通过获取这两个值我们可以做到自适应。在官方文档里,这个形参被命名为 $element。

  2. p2 里面存放了所有自定义选项的值,比如之前设置的字体颜色、字体大小,都可以在 p2 中获取。在官方文档里,这个形参被命名为 layout。

2.9 创建不同 id

考虑到可能在同一个工作表中可能会重复使用到同一种插件,对于点击事件来说,大多数情况下需要针对的是 id,而 id 是不可以重复的,所以我们可以通过下面的方式来保证 id 的唯一性。

controller: ['$scope', function ( $scope ) {
    //add your rendering code here
    $scope.html = "Hello World";
    
    let generateId = new Date().getTime();
    $scope.getId = function () {
        return generateId;
    }
}]

2.10 获取关于object更多的相关数据

这一点在 2.8 已经提到了,但是因为很重要,所以单独再提出来说一遍。

在 paint: function() 中其实是有参数的,只不过 qlik sense 把参数隐藏了,我们可以添加形参然后打印出来,查看每一个参数的数据格式。

除了可以获取 object 的相关数据,用户在自定义配置中填写所有的值都可以获取到。我们可以通过数据来编写自适应,可以及时处理数据,同样的,在实现点击跳转功能时,url 或者 sheetID 就是从这里获取的,只不过编写的代码是写在了 controller 中。

2.11 添加跳转事件

跳转事件一般有两种,一个是跳转本 App 下的其他任一 sheet 页面,另一个是跳转外部网址。

要跳转 sheet 页面,就需要获取到当前 app 下所有的 sheet 页面有那些,需要用到 app.getAppObjectList() 这个 api 方法。在 mashup 里是通过 appID 来获取到的 app,在 extension 里需要通过 qlik.currApp() 的方法来获取当前 app。同时需要引入一个 core.utils/deferred 文件。

define( ['jquery', "qlik", "core.utils/deferred", "text!./template.html"],
	function ($, qlik, Deferred, template ) {
		var app = qlik.currApp(); // 直接获取app
		return {
			template: template,
			initialProperties:{
				eventArray: [] 
                // 这里定义了一个 property,之后可以从 layout.eventArray中获取到事件相关的属性值
			},
			support:{
				... // 省略
			}, ...]
		};
	} );

之后在 definition 下,与 appearance 平级的地方添加一下代码。

events: {
        label: "事件",
        type: "array",
        ref: "eventArray", // 指向 initialProperties 定义的变量
        itemTitleRef: "name",
        allowAdd: true,
        allowRemove: true,
        addTranslation: '添加事件',
        items: {
            name: {
                ref: "name",
                label: "事件名称",
                type: "string",
                expression: "optional"
            },

            type: {
                ref: "type",
                label: "事件类型",
                component: "dropdown",
                type: "string",
                options: [{
                    value: "gotosheet",
                    label: "跳转工作表"
                },{
                    value: "gotowebSite",
                    label: "跳转网页"
                }]
            },
            sheetID: {
                ref: "sheetID",
                label: "跳转页面",
                component: "dropdown",
                type: "string",
                options: function (data) {
                    var df = Deferred();

                    app.getAppObjectList("sheet", function (reply) {
                        var sheetList = reply.qAppObjectList.qItems.map(function (sheet) {
                            return {
                                value: sheet.qInfo.qId,
                                label: sheet.qMeta.title
                            }
                        });
                        //console.log("####");
                        //console.log(sheetList);

                        df.resolve(sheetList);
                    });

                    return df.promise;
                },
                show: function (p1, p2, p3) {
                    return p1.type == "gotosheet";

                    // console.log(p1);
                    // console.log(p2);
                    // console.log(p3);
                }
            },
            webSite: {
                ref: "webSite",
                label: "网址",
                type: "string",
                defaultValue: '',
                show: function(p1){
                    return p1.type == "gotowebSite";
                }
            },
            sameWindow: {
                ref: "sameWindow",
                label: "在本页内打开",
                type: "boolean",
                defaultValue: true,
                show: function (p1) {
                    return p1.type == "gotowebSite";
                }
            },
        }
    }

在 template.html 文件里,在想要添加点击事件的 div 中添加 ng-click 方法。

<div ng-click="gotoAnywhere()" id="{{getId()}}">
    {{ html }}
</div>

之后在 controller 方法中添加对 gotoAnywhere() 函数的编写。

$scope.gotoAnywhere = function () {
    // find() 返回的是object类型,返回第一个符合条件的数据
    var res = $scope.layout.eventArray.find(function(item){
        if(item.type == 'gotosheet'){
            qlik.navigation.gotoSheet(item.sheetID);
        }else if(item.type == 'gotowebSite'){
            // console.log(item);
            var url = item.webSite.startsWith("http://") || item.webSite.startsWith("https://")   
                        ? item.webSite : "http://" + item.webSite; 
            if(!item.sameWindow)
                window.open(url, "_blank");
            else
                window.open(url, "_self");
        }
    });
};

跳转 sheet 页面调用是 Navigation API, 通过 sheetID 就可以直接跳转。但是对于网页来说,需要进行一步转换,因为用户填写的网址没有添加 http:// 或者 https://, qlik 会在自己的域名后直接拼接上 url,会导致打不开页面,所以需要转换一下 url 链接。

2.12 获取当前服务器下所有的 App 列表 以及获取对应 App 下的 Sheet 列表

otherAppID: {
    ref: "otherAppID",
    label: "其他App",
    component: "dropdown",
    type: "string",
    options: function (data) {
        //var Promise = qlik.Promise;
        var df = Deferred();
        qlik.getGlobal().getAppList(function (items) {
            var appList = items.map(function (item) {
                if(item.qDocId != qlik.currApp().id){
                    return {
                        value: item.qDocId,
                        label: item.qTitle
                    }
                }
            });
            // console.log(appList);
            df.resolve(appList);
        });
        return df.promise;
    },
    show: function (p1, p2, p3) {
        // console.log(p1);
        // console.log(p2);
        // console.log(p3);
        return p1.type == "gotoothersheet";
    }
},
otherAppSheetID:{
    ref: "otherAppSheetID",
    label: "其他App下的工作表",
    component: "dropdown",
    type: "string",
    options: function(data, p1,p2){
        // console.log("###");
        // console.log(p1.layout.eventArray[0].otherAppID);
        // console.log(p2);
        // console.log(data);
        // 这里不使用 data.otherAppID,因为选择工作表后刷新paint,导致data数据发生改变
        // 这里能成功的前提是假设只能有一个跳转事件,有多个的话,下标就会错乱
        var otherApp = qlik.openApp(p1.layout.eventArray[0].otherAppID);
        // console.log(otherApp);
        var defer = Deferred();

        otherApp.getAppObjectList("sheet", function (reply) {
            var otherSheetList = reply.qAppObjectList.qItems.map(function (sheet) {
                return {
                    value: sheet.qInfo.qId,
                    label: sheet.qMeta.title
                }
            });
            // console.log("####");
            // console.log(otherSheetList);

            defer.resolve(otherSheetList);
        });

        return defer.promise;
    },
    show: function (p1) {
        // console.log(p1);
        return p1.type == "gotoothersheet" && p1.otherAppID != "";
    }
}

2.13 跳转传参

APP 之间跳转分两种方式,直接跳转对应 app 下的某一个 sheet 页的方式或者通过 single object 的方式,分别需要用到以下 API:

  1. App Integration API

官方关于在 url 添加 selections 的例子:
http[s]://<machinename | servername>/{virtual proxy}/sense/app/{appid}/  
sheet/{sheetid}/state/analysis/select/{field}/{value1;value2}/select/{field2}/{value1;value2}

eg:
http://域名/sense/app/{appId}/sheet/{sheetId}/select/企业名称/aaa;bbb/select/年月/2019-05

如果需要先清空之前所有的选择,即每次都以本次打开的选择为优先(因为 selection 是一个全局变量,不同用户对同一个工作表的数据选择都会相互影响),可以在 url 中添加 options 变量,例子如下:

http://域名/sense/app/{appId}/sheet/{sheetId}/options/clearselections/select/企业名称/aaa;bbb/select/年月/2019-05
  1. Single Integration API

官方对于 url 中 "选项" 这一参数的解释如下:

opt Description
ctxmenu enables the context menu.
currsel displays the Selection bar.
debug starts a JavaScript debugger.The debug option can only be defined in the URL.
noanimate turns off animations.
noselections turns off selections.
nointeraction turns off interaction.

ctxmenu 即用户右键点击是的上下文菜单
nointeraction 即禁止一切交互,包括用户拖拉表格都会被禁止,所以如果要禁止用户进行一些选择上的操作,参数选择 noselections 即可

eg:支持上下文菜单(导出数据等)& 在头部显示选择项 & 禁止用户选择

http://域名/single/?appid=xxxxxxx&sheet=xxxxxx&opt=ctxmenu,currsel,noselections&select=clearall&select=fieldName,fieldValue&select=fieldName,fieldValue1,fieldValue2
posted @ 2020-10-21 17:11  猫熊小才天  阅读(614)  评论(0编辑  收藏  举报