收藏!0基礎開源資料視覺化平臺FlyFish大屏開發指南

語言: CN / TW / HK

作者:Rise Hao,雲智慧前端開發工程師。開源專案資料視覺化平臺 FlyFish Maintainer。主攻視覺化大屏方向,專注工程研發的降本增質、增效,在視覺化方面具有豐富的開發經驗 。

FlyFish 是雲智慧公司自主設計、研發的一款低門檻、高拓展性的低程式碼應用開發平臺, 為資料視覺化開發場景提供了高效的一站式解決方案。FlyFish提供豐富的元件和應用模板庫, 可通過拖拉拽的形式完成資料視覺化開發,零開發背景的使用者也可完成資料視覺化開發工作。 同時,FlyFish也提供了靈活的拓展能力,支援元件開發、自定義函式與全域性事件等配置, 面向複雜需求場景能夠保證高效開發與交付。

相關文件地址

開始前(準備)

  1. FlyFish平臺線上地址

  2. 建立專案(整體專案名稱)

    • 檢視是否有當前正要做的專案

    • 添加當前專案(如果沒有當前專案,有則忽略)

    • 新增應用(視覺化大屏)

    • 瀏覽是否有滿足UI設計的基礎元件(UI:視覺化大屏元件樣式)

開始上手(初級)

  1. 選擇要開發的應用(視覺化大屏)

  2. 選擇適合的基礎元件

    • 拖入視覺化大屏內需要擺放的位置,去選擇合適的配置滿足UI的需求

    • 如果僅通過配置項無法滿足當前元件與UI的要求,可自定義CSS,新增css名字(會新增到當前元件的最外層,並在全域性樣式內進行自定義)

    • 請求資料的方式

  3. 選擇當前專案下的元件(如果有的話...)

開始開發(中級)

  1. 有類似的專案元件(但是仍需要進行定製化的)

    • 複製此元件,並起一個新的名字

    • 編輯此元件資訊,新增到當前專案

  2. 新增定製化專案元件(如果基礎元件不具備滿足你當前的需求)

  1. 專案元件開發,選擇剛剛建立好的專案元件、點選開發元件

  1. 程式碼結構

build/webpack.config.dev.js

元件開發階段儲存對元件進行 webpack 編譯打包擴充套件配置檔案,具體請參考更改元件編譯配置

#build/webpack.config.production.js

元件匯出階段對元件進行 webpack 編譯打包擴充套件配置檔案,具體請參考更改元件編譯配置

#package.json

元件資訊和依賴,具體請參考新增元件依賴

#options.json

元件開發底部的元件預覽大屏的預設,具體請參考增加元件開發大屏預設

#src/main.js

元件註冊入口,元件開發會自動產生此檔案,如務必要不需要更改。具體請參考註冊元件

#src/Component.js

元件程式碼檔案,僅支援原生 Javascript 進行開發,請參考開發元件。如使用 react 開發,請參考React 開發元件

#src/setting.js

元件設定區域註冊入口,元件開發會自動產生此檔案,如元件有需要開發自定義配置和資料繫結,請開啟此檔案內註釋掉的註冊內容

#src/setting/options

元件設定區域元件,需使用 react 開發,具體請參考增加元件配置

#src/setting/data

元件設定資料區域元件,需使用 react 開發,具體請參考增加元件資料配置

  1. 是否需要配置模組

1 是右邊的資料請求,2 是右邊的模組配置

  1. 資料請求方式(直接在程式碼中寫)

    • 僅開發中生效--的模擬資料

    • 大屏依然生效--的模擬資料(應用 = 視覺化大屏)

    • 預設選項,沒有資料,但該引數又是必須引數(傳遞給元件的預設資料)

  2. 元件內獲取資料

    • 獲取API請求資料,直接props中獲取data

    • 獲取預設選項

  3. 安裝依賴(如果元件開發中需要引用某些外掛)

FlyFish支援通過Echarts等外部平臺開發元件,如有需要可通過引用相關外掛的方式去實現。

  1. 更新上線

開始進階(高階)

  1. 設定大屏(官方文件

  2. 事件(官方文件

    • 元件之間傳遞事件

      1.     第一個箭頭傳遞給某個元件事件以及引數
      2.     第二個箭頭可以直接觸發某個元件的資料請求

    • 元件之間接收事件

      收到了元件傳遞過來的事件則會自動執行你的自定義

  • 配置元件之間的事件(不配置不會生效喲)

    •   有了剛才元件的內部自定義的事件,我們可以設定之間的關聯

    •   如果選擇紅框框內的事件則作用在整個元件身上,如果選擇紅色箭頭的事件則按照你剛才建立的方法開始執行

    •   如果選擇紅色圓圈內的則作用於整個大屏之上,不在於某個元件內,如果選擇紅色方框則作用於所選的元件內部事件

    •   選擇剛剛定義的trigger事件

    •   接收元件定義的方法(注意不是傳送事件的那個喲,當然為了避免容易犯錯誤,你可以將兩個名字設為一致)

    •   可以選擇修改剛剛定義的事件

  1. 函式(官方文件

    自定義函式,常見的用法是提供給大屏的事件使用。

  2. 全域性資料集(官方文件

全域性資料集可以給多個元件使用

開始進階(骨灰級)

  1. 預設選項跟隨資料進行實時渲染?

    • 重寫load方法,因為他可以更新預設的選項 defaultOptions。
         /**
          * 載入資料
          * @param {Object=} options 臨時載入選項
          * @param {function(Array.<Object>)=} onSuccess 載入完成回撥
          * @param {function(string)} onError 載入失敗回撥
          * @returns {Component}
          */
         load(options = {}, onSuccess = null, onError = null) {
           if (this.hasDataSource()) {
             if (isFunction(options)) {
               /* eslint-disable no-param-reassign */
               onError = onSuccess;
               onSuccess = options;
               options = {};
               /* eslint-enable no-param-reassign */
             }
             // 載入資料事件
             this.trigger('load');
             this.dataSource.load(
               options,
               (data) => {
                 call(onSuccess, this, data);
                 let opt = this.getOptions()
                 const { lineBackgroundDefault, lineBackground } = opt;
                 const newLineBackground = data.dataList.map((_,i) => lineBackground[i] || lineBackgroundDefault);
                 // 資料載入完成事件
                 console.log(newLineBackground, '<--data')
                 this.trigger('loaded', data);
                 this.setOptions({lineBackground: JSON.parse(JSON.stringify(newLineBackground))})
                 this.draw(data);
               },
               onError
             );
           }
           return this;
         }
     ```
    
    
  2. 配置面板如何根據資料實現聯動變化?

    • 在options.js檔案寫上下面的句子就可以拿到更新之後的資料了。todo(data)
     /*
      * @Author: Rise.Hao
      * @Date: 2022-05-11 22:53:50
      * @LastEditors: Rise.Hao
      * @LastEditTime: 2022-06-01 21:33:08
      * @Description: file content
      */
    
     'use strict';
    
     import React from 'react';
     import Base from './panel/index.js'
     import { cloneDeep } from "data-vi/helpers";
     import { recursionOptions } from '@cloudwise-fe/chart-panel'
     import { ComponentOptionsSetting } from 'datavi-editor/templates';
     export default class OptionsSetting extends ComponentOptionsSetting {
       constructor(props) {
         super(props)
       }
       // 可自定義樣式: 若您在設定面板中書寫樣式會抽離出setting.css.
       // 顯式的將以下屬性設定為true可告知FlyFish來載入您的樣式檔案
       enableLoadCssFile = true;
    
       componentDidMount() {
         const { component } = this.props;
         component.bind('draw', () => {
           this.forceUpdate()
         })
       }
    
       componentWillUnmount() {
         const { component } = this.props;
         this.computedSettingStyleAppend(true);
         component.unbind('draw');
       }
    
       getTabs() {
         const options = recursionOptions(this.props.options, true)
         const {component, updateOptions} = this.props;
         return {
           config: {
             label: '配置',
             content: () => <Base initialValues={options}  props={this.props} options={cloneDeep(component.getOptions())} onChange={updateOptions} />,
           },
         }
       }
     }
     ```
    
  • 配置面板應該怎麼寫?

     /*
      * @Author: Rise.Hao
      * @Date: 2022-05-29 13:33:05
      * @LastEditors: Rise.Hao
      * @LastEditTime: 2022-06-01 22:00:47
      * @Description: file content
      */
     import React from 'react'
     import { Input, Select, ConfigProvider, InputNumber } from 'antd';
     import { ChartProvider, FormItem, FormItemGroup, CollapsePanel, Collapse, ColorPickerInput } from '@cloudwise-fe/chart-panel'
     export default function Index(props) {
       const { options, initialValues, onChange } = props;
       const { lineBackground = [] } = options || {};
    
       const lineFunc = (e, index, key) => {
         const newLineBackground = lineBackground.map((item, i) => {
           return i === index ? {
             ...item,
             [key]: e
           } : item
         })
         onChange({ lineBackground: newLineBackground })
       }
     console.log(lineBackground,'<--lineBackground')
       return <ChartProvider>
         <ConfigProvider prefixCls="ant4">
           <Collapse>
             <CollapsePanel title="面積顏色" key="1">
               <FormItemGroup layout="vertical">
                 {
                   lineBackground.map((item, index) => {
                     return <FormItem key={`background${index}`} label={`第${index + 1}條線面積顏色`}>
                       <ColorPickerInput onChange={(e) => lineFunc(e, index, 'background')} value={item.background} gradientMode="gradient" />
                     </FormItem>
                   })
                 }
               </FormItemGroup>
             </CollapsePanel>
             <CollapsePanel title="字型顏色" key="1">
               <FormItemGroup layout="vertical"  initialValues={initialValues} onValuesChange={onChange}>
                 <FormItem label="X軸字型" name="xAxisFontColor">
                   <ColorPickerInput gradientMode="gradient" />
                 </FormItem>
                 <FormItem label="Y軸字型" name="yAxisFontColor">
                   <ColorPickerInput gradientMode="gradient" />
                 </FormItem>
               </FormItemGroup>
             </CollapsePanel>
           </Collapse>
         </ConfigProvider>
       </ChartProvider>
     }
    
  1. 有些時候更改了某個配置項而他有沒有生效?

    比如:引數本身是一個數組又或者是一個物件,這個陣列本身就存在,而你此次操作只是給數組裡面刪除了一個物件,最終沒有生效。原因是FlyFish預設執行的setOptions是合併資料而不是更新資料
    • 把陣列進行字串處理,讓他變成一個值,這樣就不是合併了。
    • 重寫setOptions方法,數組裡面有的引數都執行更新操作,沒有的執行合併操作。
import { defaultsDeep } from "data-vi/helpers";

/**
     * 設定選項
     *
     * @param {Object} options 選項
     * @param {boolean} merge 是否合併原來的選項
     * @returns {Component}
     */
  setOptions(options = {}, merge = true) {
    const { replaceAll, ...mergeOptions } = options;
    const replaceKeys = ['lineBackground'];
    // 魔改一下部分結果處理
    if (replaceAll) {
      this.options = mergeOptions;
    } else if (merge) {
      let cloneOption = defaultsDeep({}, mergeOptions, this.options);
      if (replaceKeys.find((v) => typeof mergeOptions[v] !== 'undefined')) {
        cloneOption = {
          ...cloneOption,
          ...mergeOptions,
        };
      }
      this.options = cloneOption;
    } else {
      this.options = defaultsDeep({}, mergeOptions, this.getDefaultOptions());
    }
  1. 確保在所有元件載入完成後自動執行一個trigger方法?

useEffect(() => {
    if (!nowdata) return;//nowdata是請求後端返回來的資料
    if (parent && parent.screen) {
      const allComponent = parent.screen.getComponents();
      const lastComponent = allComponent[allComponent.length - 1];
      if (lastComponent.mounted) {
        parent.trigger('add', { id: currentItem, value: nowdata })
      } else {
        lastComponent.bind("mounted", () => {
          parent.trigger('add', { id: currentItem, value: nowdata })
          lastComponent.unbind("mounted");
        })
      }
    }
  }, [nowdata])
  1. 我這個元件怎麼去更改別的元件的預設選項?(謹慎操作)

const compontentList = this.props.component.screen.getComponents()
compontentList.forEach((item)=>{
//這裡可以做判斷對那個元件進行操作
    item.setConfig({
      visible: true
    })
}
  1. 建議不帶 get 的 static?

 // 預設配置 static defaultConfig = {};
getDefaultConfig() {
    return defaultsDeep({}, this.constructor.defaultConfig, {
        width: 100,
        height: 100,
        index: 0,
        left: 0,
        top: 0,
        name: '',
        visible: true,
        class: ''
    });
}
  1. 輸入框和FlyFish的事件衝突?

// 禁止冒泡掉
const bubblingFunc= (event)=>{
    event.stopPropagation();
}
<input 
onKeyUp={bubblingFunc} 
onKeyDown={bubblingFunc} 
/>
  1. 事件可以在元件裡面直接寫好了!

// 註冊事件
registerComponentEvents("id", "DEFAULT_VERSION", {
    onChange: "變更",
    onValueChange: "值變更",
});


// 註冊action
registerComponentAction("id", "DEFAULT_VERSION", "changeValue", ReactCompont);

call(component, "changeValue", ...args);

// ReactCompont;
export default (props) => (
    <Form>
        <FormItem label="橫座標(X)" cols={[8, 8]}>
            <Input
                value={toString(props.args[0])}
                placeholder="請輸入橫座標(X)"
                onChange={(event) =>
                    props.onChange(0, toNumber(event.target.value))
                }
            />
        </FormItem>
        <FormItem label="縱座標(Y)" cols={[8, 8]}>
            <Input
                value={toString(props.args[1])}
                placeholder="請輸入縱座標(Y)"
                onChange={(event) =>
                    props.onChange(1, toNumber(event.target.value))
                }
            />
        </FormItem>
    </Form>
);
  1. 靜態檔案從根目錄取絕對路徑的該如何設定?

import { DEFAULT_VERSION } from "data-vi/components";
import config from "data-vi/config";
const componentStaticDir = props.parent.getVersion() == null || props.parent.getVersion() === DEFAULT_VERSION ? "components" : "release";
  const link = `${config.componentsDir}/${props.parent.getType()}/${props.parent.getVersion() || DEFAULT_VERSION}/${componentStaticDir}/public`;
  
  //webpack.config.production.js檔案
  const CopyPlugin = require("copy-webpack-plugin");
  plugins: [
        new CopyPlugin( [
            { from: path.resolve(__dirname, '../') + '/src/ModelRotates/public', to: path.resolve(__dirname, '../') + '/components/public/', },
        ]),
    ]
    
  //安裝依賴
  "copy-webpack-plugin": "5.1.1"
  1. 元件內需要自己寫請求?

import { getHttpData } from 'data-vi/api';
import { componentApiDomain } from 'data-vi/config';

const getMapdata = (name) => {
    getHttpData(componentApiDomain + `/atlas/info?location=${encodeURIComponent(name)}`, 'GET', {})
      .done((request) => {
        console.log('請求成功', request)
        setNowdata(request.data)
      })
      .fail((request, xhr, msg) => {
        console.log('失敗了')
      });
  }
  1. 比如跳轉大屏如何根據url實現資料變更?

function preDisposeParams(params) { 
    let sumParams = window.location.search ? window.location.search.split('?')[1] : '';
    let eachParams = sumParams.split('&')[1] || '';
    let systemCode = eachParams.split('=')[1] || '';
    let jsonParams ={
      "systemCode":systemCode
    }
    console.log(sumParams,"-",eachParams,"-",systemCode)
  return jsonParams; }
  1. 整張大屏內如何使用字型?【後期可能會更改】

    •   示例元件
/**
     * 鉤子方法 元件mount掛載時呼叫
     */
    _mount() {
      const container = this.getContainer();
      console.log(this.getType(),this.getVersion(),'123')
      const componentStaticDir =
      this.getVersion() == null || this.getVersion() === DEFAULT_VERSION ? "components" : "release";
      const link = `${config.componentsDir}/${this.getType()}/${
          this.getVersion() || DEFAULT_VERSION
      }/${componentStaticDir}/assets`;
      container.html(`
        <style>
          @font-face {
            font-family: FZZYJW;
            src: url('${link}/FZZYJW.TTF');
          }
          @font-face {
            font-family: FZZZHONGJW;
            src: url('${link}/FZZZHONGJW.TTF');
          }
          @font-face {
            font-family: HYa9gj;
            src: url('${link}/hya9gjm.ttf');
          }
          @font-face {
            font-family: HYk2gj;
            src: url('${link}/HYLingXin.ttf');
          }
          @font-face {
            font-family: SourceHanSerifSCHeavy;
            src: url('${link}/SourceHanSerifSCHeavy.ttf');
          }
        </style>
      `);
    }

開發完成

  1. 點選預覽,檢視效果是否滿足,並簡單自測是否有BUG

  1. 匯出已完成的視覺化大屏

部署上線

  1. componentApiDomain = 請求後端資料的ip地址

  2. 如果nginx代理沒有從根目錄配置則需要更改

    • iplpadImgDir的路徑 = ./ (/data/app需要根據nginx代理的具體路徑來配置)

    • components的路徑= /data/app/components (/data/app需要根據nginx代理的具體路徑來配置)

  3. 標準流程 tengine部署

    • 修改前端部署包配置檔案
    • 新建前端部署資料夾 web(/data/web/ tengine部署中都以該資料夾為例)
    • 將前端包檔案screen.zip拷貝到該目錄下
    • 解壓命令:unzip screen.zip
    • 修改/data/app/sxdl_web/config/env.production.js
    • 修改/data/tengine/conf/vhost/路徑 (tengine部署目錄為/data/tengine)
    • 修改/data/app/sxdl_web/index.html (瀏覽器重新整理文字)
    • 修改/data/app/sxdl_web/config/env.conf.json (瀏覽器頁籤文字)
    • 重啟tengine服務 /data/app/tengine/sbin/nginx -s reload
    • 前端訪問地址
  4. nginx部署

    • 修改前端部署目錄xxxx/config/env.production.js配置檔案
    • 配置檔案: env.production.js:{ componentApiDomain:後端介面地址 }
    • 部署環境nginx 注意:大屏前端配置的埠不可以和其他服務的前端的埠衝突
    • 首先備份/nginx/conf下的nginx.conf
    • 編輯nginx.conf
     重啟ngnix /sbinx下 ./ngnix  -t                 //檢查配置檔案nginx.conf的正確性
     ./ngnix  -s  reload                                     //重新載入配置檔案 
     ```
    
    
  5. 上傳screen.zip file.dir: /var/www/html 將解壓後screen.zip檔案放入該目錄(先清空html資料夾)

    備註:如果沒有相同的路徑,則隨便找個路徑放檔案就行

  6. 解壓screen.zip檔案

  7. 修改/var/www/html/env.production.js檔案 \修改配置文件: env.production.js

  8. 修改配置後,重啟nginx

  9. 專案執行地址: 伺服器地址+’/index.html’

    (注:前端本次用FlyFish開發頁面,直接打出包,git上無倉庫)

下載原始碼

  1. 字首:http://10.0.16.230:7001/applications/export-source/演示樣例,真實連結會根據FlyFish本地的地址變化

  2. 大屏的ID

  1. 最終下載地址(演示樣例,非真實地址):http://10.0.16.230:7001/applications/export-source/62134fbaddc0c8314cd3be30

    注:字首 + 大屏ID = 下載地址(請謹慎頻繁呼叫)

    Echarts配置及匯出(如有下載失敗,請更換版本號):https://www.npmjs.com/package/@cloudwise-fe/chart-panel

  2. 安裝依賴: yarn 或 npm install

  3. 啟動專案:yarn dev 或 npm run dev

  4. 再次編譯:yarn build 或 npm run build