所有文档

          物可视 IOTVIZ

          离线模式实践

          自从天工物可视公测以来,已经有不少用户向我们反映,因为自身的数据安全需求或者法律法规的要求,他们无法使用运营在公开网络环境中的可视化产品。然而此前物可视并不具备脱离公网运行的能力,使得使用百度天工物联网整体解决方案的用户在可视化的环节缺了一段,甚为可惜。

          好在现在物可视已经推出了“离线模式”,允许用户将仪表盘打包成离线模式运行在内网环境。离线模式主要有以下几个特点:

          1. 使用离线模式任务会得到一个 zip 包,通过使用本地静态服务器(如 nodejs 的 http-server, nginx 等)在本地启动服务即可
          2. 不向公网发送任何请求,可以部署在私有化网络中。
          3. 可以通过 PlayerAPI 由用户自行获取并更新数据,灵活性强

          更多离线模式概念性的说明可以参考文档。本文的主要目的是为了指导用户如何使用物可视离线模式构建运行于内网的可视化应用。

          除了离线模式本身的操作流程之外,本文还会包含获取/更新数据等操作,是一个完整的操作流程。毕竟用户使用离线模式不是为了使用静态数据做 Demo,而是需要真实的内网数据做展示的。

          要使用物可视从零开始搭建一个内网可用的可视化产品,主要有如下几个大的步骤:

          1. 申请物可视离线模式使用权限
          2. 创建并编辑仪表盘(也可以使用已有的仪表盘)
          3. 创建离线任务获取离线包(本文重点
          4. 编写自定义脚本获取数据,并更新到离线包中(详细可参考使用 PlayerAPI 更新数据,本文只作简单说明)
          5. 运行离线包(本文重点

          下面我们逐步讲解。

          申请离线模式使用权限

          公测期间,离线模式采用用户白名单的方式运行。想要体验离线模式的用户,可以通过提交工单的方式联系我们开通,我们会将您的账号添加到白名单中。

          待开通完成后,您可以在仪表盘编辑界面的右上角找到“离线模式”的按钮,这样就证明您已经拥有离线模式的使用权限了。

          image.png

          我们欢迎您申请试用,并给我们反馈您在使用中遇到的任何问题或者改进意见,这些对于我们提升产品功能和体验至关重要!

          创建并编辑仪表盘

          拥有了离线模式的使用权限后,下一步就和开发普通流程类似,创建并编辑一个仪表盘。如果您已有一个编辑好的仪表盘,也可以直接使用。

          不论通过新建/复制/直接使用的方式,假设我们已经拥有一个仪表盘了。为了后续方便说明,我们以如下仪表盘为例,接下来我们就要为它创建离线模式运行包了。

          image.png

          注意:

          1. 仪表盘中的图表和标题都绑定了数据源,会跟随数据变化而变化
          2. 我们使用了一张背景图。因为离线模式无法访问公网,因此在打包后这张背景图也会一并被加入到离线包中

          image.png

          再看看数据源的配置。我们创建了一个仿真数据源,随机生成“城市”和“温度”两列数据。之后创建了一个流水线步骤,新建了一列“体感”,根据温度取值返回“炎热”,“舒适”或者“寒冷”。(上图仪表盘中的标题就是绑定了上海的体感字段)

          这里也有两个注意点:

          1. 如果想在之后从其他 API 获取数据的话,这里必须选择静态数据源,否则仿真数据源本身的自动更新会和 PlayerAPI 互相冲突,引发混乱。
          2. 记录下这个数据源的名字,之后会用到。例如我们这里叫做“本地气温记录”。

          创建离线任务获取离线包

          1. 点击仪表盘编辑界面,画布标签页右上角的“离线模式”按钮

            image.png

          2. 在弹出的离线任务弹框中,在第一个标签页(创建离线模式运行包,默认选中)中查看相关的说明后,点击“创建离线包”按钮

            image.png

          3. 点击后,系统会先进行相关的检查,例如用户的操作权限,用户的每日创建限额(当前限定为每日 5 个)等等。待这些通过后,则开始正式创建,我们能看到一个加载动画。整个过程的持续时间应该在 1 分钟左右。

            同一个用户同一时间只能创建一个打包任务,必须待其完成后才能创建一个新的。因此如果您的上一个任务还在运行中,打开弹框后会看到上一个任务,暂时不能创建。

            为了以防万一(如网络问题或者服务器故障等),如果一个运行中的任务运行超过 10 分钟,用户可以通过点击加载动画下方的“中止任务”来中止。当然如果重试依然无效,您可以提交工单来向我们汇报这个问题。

          4. 任务完成后,会显示一个“下载离线包”的按钮。点击后会下载一个 zip 文件。

            image.png

            如果不小心关闭了这个弹窗,或者想下载之前执行过的离线包,也可以切换到离线模式弹窗的第二个标签页,下载离线模式运行包。

            image.png

            选择某个离线包后,点击右下角的下载即可。

          5. 解压 zip 包,可以看到如下几个文件

            image.png

            在运行前请优先阅读 README.md。尤其需要注意的是,启动时端口号是 9100

            另外值得我们注意的是,由于刚才我们的仪表盘使用了一张图片作为画布背景,这里能看到 images 目录中就存放了这张图,方便离线模式本地引用。如果有使用其他图片也会被一起放到这里,并重新命名,防止命名冲突。

          至此我们已经准备好了离线包,但这个离线仪表盘的数据源是一个静态数据源。如果您的需求是要使用静态数据源或者仿真数据源进行 Demo 的,那么直接运行即可(如何运行在本文最后一节有涉及);如果您想获取内网的其他接口来获取数据并更新图表的话,请继续看下一节。

          获取内网数据并更新图表

          接下来我们要访问内网 API 获取数据,并使用使用 PlayerAPI 来更新图表。严格来说这并不是离线模式的功能,但通常它会配合离线模式一起使用,因此我们在这里一并了解一下。有关它的更详细的解释在这篇文档中。

          另外还需要说明的是,使用 PlayerAPI 更新图表时,因为发送请求获取数据是由用户控制和负责的,因此物可视并不关心数据的来源,只关心数据的格式(必须和要更新的图表绑定的数据源格式一致)。换句话说,使用 PlayerAPI 更新图表时,物可视可以搭配任何数据源使用。可以是天工的时序数据库或者物影子,也可以是其他用户自行编写的数据接口,更可以是其他云产品的服务,只要能够正常返回数据即可。

          最后一点,在上面创建仪表盘一节已经提过,如果想使用 PlayerAPI 更新图表的话,请在仪表盘中使用静态数据源,否则会和仿真自身的更新冲突,引发混乱。

          我们假设数据接口的地址是 /data/getTemperature, 我们需要对离线包的 index.html 进行编辑:( index.html 是由 webpack 压缩生成,因此格式上不太可读,请自行添加换行)

          1. 首先我们需要添加发送请求获取接口数据的类库,例如 axios。我们从它的下载地址(https://unpkg.com/axios/dist/axios.min.js)下载下来后放到 static 目录,通过 static/axios.min.js 来访问到它。
          2. <head> 标签中添加 <script src="static/axios.min.js"></script> 以引用刚才我们添加的 axios 类库。
          3. 在文件接近末尾的地方我们可以发现这样一段代码

            var myDashboard = bdIotVizPlayer({
                fillMode: 'none', // none, responsive, fill, cover, contain
                containerElement: containerElement,
                dashboardSpec: "...", // 很长一段字符串,内容可忽略,也不必修改
                dashboardName: '未命名仪表盘',
                dashboardSign: '...' // 不太长的一段字符串,也可以忽略
            });
            // 后面还有 </script></body></html> 这样的 HTML 结束标签

            在这段代码之后,</script> 等结束标签之前添加如下代码:

            var myDashboard = bdIotVizPlayer({
                fillMode: 'none', // none, responsive, fill, cover, contain
                containerElement: containerElement,
                dashboardSpec: "...", // 很长一段字符串,内容可忽略,也不必修改
                dashboardName: '未命名仪表盘',
                dashboardSign: '...' // 不太长的一段字符串,也可以忽略
            });
            
            // ******上面的代码保持原样不动,改变从下面开始******
            
            myDashboard.getDashboardConfig().then(function(config){
                let dataTables = config.dataTables;
            
                // 寻找并赋值
                let temperatureData;
                dataTables.forEach(data => {
                    if (data.name === '本地气温记录') { // 使用数据源的名字,我们例子中叫做“本地气温记录”
                        temperatureData = data;
                    }
                });
            
                function startTimer() {
                    setTimeout(() => {
                        // 获取数据
                        getTemperatureData().then(data => {
                            temperatureData.config.source = data;
                            // 更新数据
                            return myDashboard.updateDataTableConfig(temperatureData.id, temperatureData.config);
                        }).catch(message => {
                            // 错误处理
                            console.log(message);
                        }).finally(() => {
                            // 下一个周期
                            startTimer();
                        })
                    }, 3000);
                }
            
                // 周期性执行任务
                startTimer();
            });
            
            function getTemperatureData() {
                return new Promise((resolve, reject) => {
                    axios.get('/data/getTemperature').then((response) => {
                        if (response.success) {
                            resolve(response.data); // response.data 的格式为 [{location: 'xxx', temperature: 0}, ...]
                        } else {
                            reject(response.message); // 失败时返回错误信息
                        }
                    });
                });
            }
            // 后面还有 </script></body></html> 这样的 HTML 结束标签

            逐行的代码解释这里就不展开了,在 使用 PlayerAPI 更新数据 一文中有详细的介绍。我们列几个注意点:

            1. 跨域问题:前端代码发送请求只能获取同域的资源,因此必须要求数据接口和前端代码部署在同域中。这个问题有多种解法,而且也是一个比较重要的问题,我们将在文末给出几种可行的方案。
            2. 后端 API 返回的数据格式必须与图表组件需要的数据格式一致。如果不一致,需要一层转换(前后端均可),否则将无法生效。
            3. 可以使用任何允许发送 HTTP 请求的方法,例如原生的 fetch 或者其他类库替代例子中的 axios。
            4. 例子中每 3 秒访问一次,可以根据业务逻辑自行修改访问频率。另外包括错误处理,接口参数等也可自行修改。

          至此我们对离线包做了小幅修改,算是把最终的运行包准备好了,最后一步就是如何运行它了。

          运行离线包

          离线包是由 HTML, JavaScript 和 CSS 组成的前端静态包,因此可以使用静态服务器启动。启动端口默认是 9100

          考虑到前面提到的跨域问题,我们总结了几种可以解决跨域的,和后端配合的部署方式。

          1. 不需要后端服务,直接使用静态/仿真数据源启动(也没有跨域问题)

            这种最为简便,多用作 Demo。可以使用任何的静态服务器启动,例如 nodejs 的 http-server

            在安装完成后,使用命令 http-server -p 9100 启动即可。

            image.png

            访问效果

            image.png

            命令行日志

          2. 需要后端数据服务,但只有一个服务。

            为了快速解决跨域问题,将前端服务和后端服务部署到同域即可。离线模式运行包全部都是静态文件,可以作为后端项目的一部分部署到同一个域名和端口,这样就不存在跨域问题了。

            前端的默认启动端口是 9100,因此可以把后端启动端口也设置为 9100。如果后端端口不能改变,那么需要在前端离线包中全文搜索 :9100,并替换为后端端口号。

            这种方式适用于一个数据后端,这样前后端就可以保持同域。但如果本身就存在多个数据后端且互相不同域,那前端到底跟谁保持一致呢?

          3. 需要多个后端服务,且他们本身就不在同一个域。

            这时就需要神器 nginx 登场。

            通过 nginx 的配置,可以把请求代理到其他域名,让浏览器以为是同域,实际却是跨域。

            假设我们有 2 个后端服务,他们分别在 8000 和 8001 提供服务。大致配置如下:(只列出接口代理的关键部分)

            http {
                # 其他配置省略
            
                # 定义两个服务的所有访问地址以及负载均衡方式
                upstream server1 {
                    ip_hash
                    server my.first.host.com:8000
                    server my.second.host.com:8000
                }
            
                upstream server2 {
                    ip_hash
                    server my.first.host.com:8001
                    server my.second.host.com:8001
                }
            
                server {
                    listen      9100 # 端口号
                    server_name localhost
            
                    location /data/getTemperature { # 接口1
                        proxy_pass http://server1/data/getTemperature # 访问服务1
                        proxy_pass_request_headers on
                        proxy_pass_request_body on
                    }
            
                    location /data/getLocation { # 接口2
                        proxy_pass http://server2/data/getLocation # 访问服务2
                        proxy_pass_request_headers on
                        proxy_pass_request_body on
                    }
                }
            }

            当后端服务本身具有多个域名时,已经不可能让前端和他们同时保持一致,因此只能通过代理的方式“欺骗”浏览器。如上配置因为考虑了服务可用性,每个服务有 2 个域名,因此还使用了 ip 轮询的负载均衡(ip_hash)。实际配置可根据用户自己的需求灵活调整,总之对前端接口代理后使之“看上去”同域即可。

          上一篇
          使用PlayerAPI更新数据
          下一篇
          组件之间的协同工作