如何實現巡檢報告?

来源:https://www.cnblogs.com/dtux/archive/2023/06/30/17516661.html
-Advertisement-
Play Games

# 什麼是巡檢報告 巡檢報告是指對某一個系統或設備進行全面檢查,並把檢查結果及建議整理成報告的過程。 巡檢報告通常用於評估系統或設備的運行狀況與性能,以發現問題、優化系統、提高效率、降低故障率等方面提供參考。 ![file](https://img2023.cnblogs.com/other/233 ...


什麼是巡檢報告

巡檢報告是指對某一個系統或設備進行全面檢查,並把檢查結果及建議整理成報告的過程。

巡檢報告通常用於評估系統或設備的運行狀況與性能,以發現問題、優化系統、提高效率、降低故障率等方面提供參考。

file

要實現什麼功能

file

自定義佈局

  1. 現報告中的面板可進行拖拽改變佈局。
  2. 在拖拽的過程中限制拖拽區域,只允許在同一父級內進行拖拽,不允許跨目錄移動,不允許改變目錄的級別,比如把一級目錄移動到另一個一級目錄內,變成二級目錄

目錄可收縮展開

  1. 目錄支持收縮展開,收縮時隱藏所以子面板,展開時顯示所以子面板
  2. 移動目錄時,子面板跟隨移動
  3. 改變目錄後,同步更新右側的目錄面板
  4. 生成目錄編號

file

右側目錄樹

  1. 生成目錄編號
  2. 支持錨點滾動
  3. 支持展開收縮
  4. 與左側報告聯動

file

數據面板

  1. 根據日期範圍獲取指標數據
  2. 通過圖表的形式展示指標信息
  3. 查看詳情,刪除
  4. 各面板的請求設計,支持刷新請求

數據面板

面板詳情

面板導入

  1. 統計目錄下選擇的面板數量
  2. 導入新面板時,不能破壞已有佈局,新面板只能跟在舊面板後
  3. 導入已有面板時,需要進行數據比較,有數據變更需要重新獲取最新的數據

file

保存

在保存前,所有影響佈局相關的操作,都是臨時的,包括導入面板。只有在點擊保存後,才會把當前數據提交給後端進行保存。

支持 pdf 和 word 導出

file

巡檢報告實現方案

數據結構設計

先看看使用扁平結構下的

file

在扁平結構下,確定子項只需要找到下一個 row 面板,對於多級目錄下也是同理,只是對一級目錄需要額外處理。
這種結構上實現簡單,但是需求要求我們限制目錄的拖拽,限制目錄需要一個比較清晰的面板層級關係,很顯然,用樹能夠很清晰的描述一個數據的層級結構

file

組件設計

與傳統組件編程有所區別。

在實現上對渲染和數據處理進行了分離,分為兩塊:

  • react 組件:主要負責頁面渲染
  • class : 負責數據的處理

file

DashboardModel

class DashboardModel {
    id: string | number;
    panels: PanelModel[]; // 各個面板
		// ...
}

PanelModel

class PanelModel {
    key?: string;
    id!: number;
    gridPos!: GridPos; // 位置信息
    title?: string;
    type: string;
    panels: PanelModel[]; // 目錄面板需要維護當前目錄下的面板信息
    // ...
}

每一個 Dashboard 組件對應一個 DashboardModel,每一個 Panel 組件對應一個 PanelModel。

react 組建根據類實例中的數據進行渲染。

實例生產後,不會輕易的銷毀,或者改變引用地址,這讓依賴實例數據進行渲染的 React 組件無法觸發更新渲染。

需要一個方式,在實例內數據發生改變後,由我們手動觸發組件的更新渲染。

組件渲染控制

由於我們採用的是 hooks 組件,不像 class 組件有 forceUpdate 方法觸發組件的方法。

而在 react18 中有一個新特性 useSyncExternalStore,可以讓我們訂閱外部的數據,如果數據發生改變了,會觸發組件的渲染。

實際上 useSyncExternalStore 觸發組件渲染的原理就是在內部維護了一個 state,當更改了 state 值,引起了外部組件的渲染。

基於這個思路簡單的實現了一個能夠觸發組件渲染的 useForceUpdate 方法。

export function useForceUpdate() {
    const [_, setValue] = useState(0);
    return debounce(() => setValue((prevState) => prevState + 1), 0);
}

雖說實現了 useForceUpdate,但是在實際使用的過程中,還需要在組件銷毀時移除事件。

useSyncExternalStore 已經內部已經實現了,直接使用即可。

useSyncExternalStore(dashboard?.subscribe ?? (() => {}), dashboard?.getSnapshot ?? (() => 0));
useSyncExternalStore(panel?.subscribe ?? (() => {}), panel?.getSnapshot ?? (() => 0));

根據useSyncExternalStore使用,分別添加了 subscribe 和 getSnapshot 方法。

class DashboardModel {	// PanelModel 一樣 
    count = 0;
    forceUpdate() {
    this.count += 1;
    eventEmitter.emit(this.key);
    }

    /**
     * useSyncExternalStore 的第一個入參,執行 listener 可以觸發組件的重渲染
     * @param listener
     * @returns
     */
    subscribe = (listener: () => void) => {
        eventEmitter.on(this.key, listener);
        return () => {
            eventEmitter.off(this.key, listener);
        };
    };

    /**
     * useSyncExternalStore 的第二個入參,count 在這裡改變後觸發diff的通過。
     * @param listener
     * @returns
     */
    getSnapshot = () => {
        return this.count;
    };
}

當改變數據後,需要觸發組件的渲染,只需要執行forceUpdate 即可。

面板拖拽

市面上比較大眾的拖拽插件有以下幾個:

  • react-beautiful-dnd
  • react-dnd
  • react-grid-layout

經過比較後,發現 react-grid-layout 非常適合用來做面板的拖拽功能,react-grid-layout 本身使用簡單,基本無上手門檻,最終決定使用 react-grid-layout 詳細說明可以查看以下鏈接:

react-grid-layout

在面板佈局改變後觸發react-grid-layoutonLayoutChange 方法,可以拿到佈局後的所有面板最新的位置數據。

const onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
    for (const newPos of newLayout) {
        panelMap[newPos.i!].updateGridPos(newPos);
    }
    dashboard!.sortPanelsByGridPos();
};

panelMap 是一個 map,key 為 Panel.key, value 為面板。是在我們組件渲染時就已經準備好了。

const panelMap: Record<PanelModel['key'], PanelModel> = {};

可以通過 panelMap 找到對應的面板,執行面板的 updateGridPos 方法進行更新面板的佈局數據。
到這,我們只是完成了面板本身數據更新,還需要執行儀錶盤的 sortPanelsByGridPos 方法,對所有的面板進行排序。

class DashboardModel {
    sortPanelsByGridPos() {
        this.panels.sort((panelA, panelB) => {
            if (panelA.gridPos.y === panelB.gridPos.y) {
                return panelA.gridPos.x - panelB.gridPos.x;
            } else {
                return panelA.gridPos.y - panelB.gridPos.y;
            }
        });
    }
    // ...
}

面板拖動範圍

目前的拖動範圍是整個儀錶盤,可隨意拖動,如下:

file

綠色是儀錶盤可拖拽區域,灰色為面板。

如果需要限制就需要改成如下的結構:

file

在原本的基礎上,以目錄為單位區分,綠色為整體的可移動區域,黃色為一級目錄塊,可在綠色區域拖動,拖動時以整個黃色塊進行拖動,紫色為二級目錄塊,可在當前黃色區域內拖動,不可脫離當前黃色塊,灰色的面板只能在當前目錄下拖動。

在原先數據結構基礎上進行改造:

file

class PanelModel {
    dashboard?: DashboardModel; // 當前目錄下的 dashboard
    // ...
}

目錄

目錄收縮展開

為目錄面板維護一個 collapsed屬性用來控制面板的隱藏顯示

class PanelModel {
    collapsed?: boolean; // type = row
    // ...
}

// 組件渲染
{!collapsed && <DashBoard dashboard={panel.dashboard} serialNumber={serialNumber} />}

對目錄進行收縮展開時,會改變自身的高度,現在還需要把這個改變的高度同步給上一級的儀錶盤。
上一級需要做的就是類似我們控制目錄的處理。如下,控制第一個二級目錄收縮:

file

當面板發生變更時,需要通知上級面板,進行對應的操作。

file

增加一個 top 用來獲取到父級實例。

class DashboardModel {
    top?: null | PanelModel; // 最近的 panel 面板

    /**
     * 面板高度變更,同步修改其他面板進行對應高度 Y 軸的變更
     * @param row 變更高度的 row 面板
     * @param h 變更高度
     */
    togglePanelHeight(row: PanelModel, h: number) {
        const rowIndex = this.getIndexById(row.id);

        for (let panelIndex = rowIndex + 1; panelIndex < this.panels.length; panelIndex++) {
            this.panels[panelIndex].gridPos.y += h;
        }
        this.panels = [...this.panels];

        // 頂級 dashBoard 容器沒有 top
        this.top?.changeHeight(h);
        this.forceUpdate();
    }
    // ...
}

class PanelModel {
    top: DashboardModel; // 最近的 dashboard 面板

    /**
     * @returns h 展開收起影響的高度
     */
    toggleRow() {
        this.collapsed = !this.collapsed;
        let h = this.dashboard?.getHeight();
        h = this.collapsed ? -h : h;
        this.changeHeight(h);
    }

    /**
     *
     * @param h 變更的高度
     */
    changeHeight(h: number) {
        this.updateGridPos({ ...this.gridPos, h: this.gridPos.h + h }); // 更改自身面板的高度
        this.top.togglePanelHeight(this, h); // 觸發父級變更
        this.forceUpdate();
    }
    // ...
}

整理流程與冒泡類型,一直到最頂級的 Dashboard。

file

展開收縮同理。

面板的刪除

對於面板的刪除,我們只需要在對應的 Dashboard 下進行移除,刪除後會改變當前 Dashboard 高度,這塊的處理與上面的目錄收縮一致。

class DashboardModel {
    /**
     * @param panel 刪除的面板
     */
    removePanel(panel: PanelModel) {
        this.panels = this.filterPanelsByPanels([panel]);

        // 冒泡父容器,減少的高度
        const h = -panel.gridPos.h;
        this.top?.changeHeight(h);

        this.forceUpdate();
    }

    /**
     * 根據傳入的面板進行過濾
     * @param panels 需要過濾的面板數組
     * @returns 過濾後的面板
     */
    filterPanelsByPanels(panels: PanelModel[]) {
        return this.panels.filter((panel) => !panels.includes(panel));
    }
    // ...
}

面板的保存

PS:與後端溝通後,當前巡檢報告數據結構由前端自主維護,最終給後端一個字元串就好。
獲取到目前的面板數據,用 JSON 進行轉換即可。

面板的信息獲取過程,先從根節點出發,遍歷至葉子結點,再從葉子結點開始,一層層向上進行返回,也就是回溯的過程。

class DashboardModel {
    /**
     * 獲取所有面板數據
     * @returns
     */
    getSaveModel() {
        const panels: PanelData[] = this.panels.map((panel) => panel.getSaveModel());
        return panels;
    }
    // ...
}

// 最終保存時所需要的屬性,其他的都不需要
const persistedProperties: { [str: string]: boolean } = {
    id: true,
    title: true,
    type: true,
    gridPos: true,
    collapsed: true,
    target: true,
};

class PanelModel {
    /**
     * 獲取所有面板數據
     * @returns
     */
    getSaveModel() {
        const model: any = {};

        for (const property in this) {
            if (persistedProperties[property] && this.hasOwnProperty(property)) {
                model[property] = cloneDeep(this[property]);
            }
        }
        model.panels = this.dashboard?.getSaveModel() ?? [];

        return model;
    }
    // ...
}

面板

面板的導入設計

file

後端返回的數據是一顆有著三級層級的樹,我們拿到後,在數據上維護成 moduleMapdashboardMappanelMap 3個Map。

import { createContext } from 'react';

export interface Module { // 一級目錄
    key: string;
    label: string;
    dashboards?: string[];
    sub_module?: Dashboard[];
}

export interface Dashboard { // 二級目錄
    key: string;
    dashboard_key: string;
    label: string;
    panels?: number[];
    selectPanels?: number[];
    metrics?: Panel[];
}

export interface Panel {
    expr: Expr[]; // 數據源語句信息
    label: string;
    panel_id: number;
}

type Expr = {
    expr: string;
    legendFormat: string;
};

export const DashboardContext = createContext({
    moduleMap: new Map<string, Module>(),
    dashboardMap: new Map<string, Dashboard>(),
    panelMap: new Map<number, Panel>(),
});

我們在渲染模塊時,遍歷 moduleMap ,並通過 Module 內的dashboards信息找到二級目錄。

在交互上設置一級目錄不可選中,當選中二級目錄時,通過二級目錄 Dashboardpanels 找到相關的面板渲染到右側區域。
對於這3個Map的操作,維護在 useHandleData中,導出:

{
    ...map, // moduleMap、dashboardMap、panelMap
    getData, // 生成巡檢報告的數據結構
    init: initData, // 初始化 Map
}

面板選中回填

在進入面板管理時,需要回填已選中的面板。我們可以通過 getSaveModel 獲取到當前巡檢報告的信息。把對應的選中信息存放到 selectPanels 中。

現在我們只需要改變 selectPanels 中的值,就可以做到對應面板的選中。

面板選中重置

直接遍歷 dashboardMap,並把每個selectPanels重置。

dashboardMap.forEach((dashboard) => {
    dashboard.selectPanels = [];
});

面板插入

在我們選中面板後,對選中面板進行插入時,有幾種情況:

  • 巡檢報告原本存在的面板,這次也選中,在插入時會比較數據,如果數據發生改變,需要根據最新的數據源信息進行請求,並渲染。
  • 巡檢報告原本存在的面板,這次未選中,在插入時,需要刪除掉未選中的面板。
  • 新選中的面板,在插入時,在對應目錄的末尾進行插入。

添加新面板需要,與目錄收縮類似,不同的是:

  1. 目錄收縮針對只有一個目錄,而插入在針對的是整體。
  2. 目錄收縮是直接從子節點開始向上冒泡,而插入是先從根節點開始向下插入,插入完成後在根據最新的目錄數據,更新一遍佈局。
class DashboardModel {
    update(panels: PanelData[]) {
        this.updatePanels(panels); // 更新面板
        this.resetDashboardGridPos(); // 重新佈局
        this.forceUpdate();
    }

    /**
     * 以當前與傳入的進行對比,以傳入的數據為準,併在當前的順序上進行修改
     * @param panels
     */
    updatePanels(panels: PanelData[]) {
        const panelMap = new Map();
        panels.forEach((panel) => panelMap.set(panel.id, panel));
        this.panels = this.panels.filter((panel) => {
            if (panelMap.has(panel.id)) {
                panel.update(panelMap.get(panel.id));
                panelMap.delete(panel.id);
                return true;
            }
            return false;
        });
        panelMap.forEach((panel) => {
            this.addPanel(panel);
        });
    }

    addPanel(panelData: any) {
        this.panels = [...this.panels, new PanelModel({ ...panelData, top: this })];
    }

    resetDashboardGridPos(panels: PanelModel[] = this.panels) {
        let sumH = 0;
        panels?.forEach((panel: any | PanelModel) => {
            let h = ROW_HEIGHT;
            if (isRowPanel(panel)) {
                h += this.resetDashboardGridPos(panel.dashboard.panels);
            } else {
                h = panel.getHeight();
            }

            const gridPos = {
                ...panel.gridPos,
                y: sumH,
                h,
            };
            panel.updateGridPos({ ...gridPos });
            sumH += h;
        });
        return sumH;
    }
}

class PanelModel {
    /**
     * 更新
     * @param panel
     */
    update(panel: PanelData) {
        // 數據源語句發生變化需要重新獲取數據
        if (this.target !== panel.target) {
            this.needRequest = true;
        }
        this.restoreModel(panel);
        if (this.dashboard) {
            this.dashboard.updatePanels(panel.panels ?? []);
        }
        this.needRequest && this.forceUpdate();
    }
}

面板請求

needRequest 控制面板是否需要進行請求,如果為 true 在面板下一次進行渲染時,會進行請求。
請求的處理也放在了 PanelModel 中。(是否單獨維護請求的邏輯?)

import { Params, params as fetchParams } from '../../components/useParams';

class PanelModel {
    target: string; // 數據源信息
    getParams() {
        return {
            targets: this.target,
            ...fetchParams,
        } as Params;
    }
    request = () => {
        if (!this.needRequest) return;
        this.fetchData(this.getParams());
    };
    fetchData = async (params: Params) => {
        const data = await this.fetch(params);
        this.data = data;
        this.needRequest = false;
        this.forceUpdate();
    };
    fetch = async (params: Params) => { /* ... */ }
}

我們數據渲染組件一般層級較深,而請求時會需要時間區間等外部參數。對於這部分參數採用全局變數的方式,用 useParams 進行維護。上層組件使用 change 修改參數,數據渲染組件根據拋出的 params 進行請求。

export let params: Params = {
    decimal: 1,
    unit: null,
};

function useParams() {
    const change = (next: (() => Params) | Params) => {
        if (typeof next === 'function') params = next();
        params = { ...params, ...next } as Params;
    };

    return { params, change };
}

export default useParams;

面板刷新

file

從根節點向下查找,找到葉子節點,在觸發對應的請求。

class DashboardModel {
    /**
     * 刷新子面板
     */
    reloadPanels() {
        this.panels.forEach((panel) => {
            panel.reload();
        });
    }
}

class PanelModel {
    /**
     * 刷新
     */
    reload() {
        if (isRowPanel(this)) {
            this.dashboard.reloadPanels();
        } else {
            this.reRequest();
        }
    }
    reRequest() {
        this.needRequest = true;
        this.request();
    }
}

右側目錄渲染

錨點/序號

錨點採用 Anchor + id 選中組件。

序號根據每次渲染進行生成。

採用發佈訂閱管理渲染

每當儀錶盤改變佈局的動作時,右側目錄就需要進行同步更新。而任意一個面板都有可能需要觸發右側目錄的更新。
如果我們採用實例內維護對應組件的渲染事件,有幾個問題:

  1. 需要進行區分,比如刷新面板時,不需要觸發右側目錄的渲染。
  2. 每個面板如何訂閱右側目錄的渲染事件?

最終採用了發佈訂閱者模式,對事件進行管理。

class EventEmitter {
    list: Record<string, any[]> = {};

    /**
     * 訂閱
     * @param event 訂閱事件
     * @param fn 訂閱事件回調
     * @returns
     */
    on(event: string, fn: () => void) {}

    /**
     * 取消訂閱
     * @param event 訂閱事件
     * @param fn 訂閱事件回調
     * @returns
     */
    off(event: string, fn: () => void) {}

    /**
     * 發佈
     * @param event 訂閱事件
     * @param arg 額外參數
     * @returns
     */
    emit(event: string, ...arg: any[]) {
}
eventEmitter.emit(this.key); // 觸發麵板的訂閱事件
eventEmitter.emit(GLOBAL); // 觸發頂級訂閱事件,就包括右側目錄的更新

面板詳情展示

file

對面板進行查看時,可修改時間等,這些操作會影響到實例中的數據,需要對原數據與詳情中的數據進行區分。

通過對原面板數據的重新生成一個 PanelModel 實例,對這個實例進行任意操作,都不會影響到原數據。

const model = panel.getSaveModel();
const newPanel = new PanelModel({ ...model, top: panel.top }); // 創建一個新的實例
setEditPanel(newPanel); // 設置為詳情

dom上,詳情頁面是採用絕對定位,覆蓋著巡檢報告。

pdf/word 導出

pdf 導出由 html2Canvas + jsPDF 實現。需要註意的是,當圖片過長pdf會對圖片進行切分,有可能出現切分的時內容區域。

需要手動計算面板的高度,是否超出當前文檔,如果超出需要我們提前進行分割,添加到下一頁中。
儘可能把目錄面板和數據面板一塊切分。

word 導出由 html-docx-js 實現, 需要保留目錄的結構,並可以在面板下添加總結,這就需要我們分別對每一個面板進行圖片的轉換。

實現的思路是根據 panels 遍歷,找到目錄面板就是用 h1、h2 標簽插入,如果是數據面板,在數據面板中維護一個 ref 的屬性,能讓我們拿到當前面板的 dom 信息,根據這個進行圖片轉換,併為 base64 的格式(word 只支持 base64 的圖片插入)。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • > 你準備好面試了嗎?這裡有一些面試中可能會問到的問題以及相對應的答案。如果你需要更多的面試經驗和麵試題,關註一下"張飛的豬大數據分享"吧,公眾號會不定時的分享相關的知識和資料。 [TOC] ## 1、談談Hadoop序列化和反序列化及自定義bean對象實現序列化? 1)序列化和反序列化 (1)序列 ...
  • 在平時和開發者們交流的過程中,發現許多開發朋友尤其是新入門 [Taier](https://github.com/DTStack/Taier) 的開發者,對於本地調試都有著諸多的不理解和問題。本文就大家平時問的最多的三個問題,服務編譯,配置&本地運行,如何在 Taier 運行 [Flink-stan ...
  • Kafka 的核心功能是高性能的消息發送與高性能的消息消費。Kafka 名字的由來是 Kafka 三位原作者之一 Jay Kreps 說 Kafka 系統充分優化了寫操作,所以用一個作家的名字來命名很有意義,他非常喜歡作家 Franz Kafka,並且用 Kafka 命名開源項目很酷 。本文是 Ka... ...
  • # 使用PySpark ## 配置python環境 在所有節點上按照python3,版本必須是python3.6及以上版本 ```Shell yum install -y python3 ``` 修改所有節點的環境變數 ```Shell export JAVA_HOME=/usr/local/jdk ...
  • 1、查找mongodb相關鏡像 docker search mongo 找到相關的鏡像進行拉取,如果不指定版本,預設下載最新的mongoDB。建議自己先查找需要那個版本後在進行拉取,因為mongoDB不同版本之間差距較大。 2、拉取鏡像 這裡拉取mongodb6.0 docker pull mong ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 界面無滾動條 滾動條的優化也有很多種,比如隨便再網上搜索美化瀏覽器滾動條樣式,就會出現些用css去美化滾動條的方案。 那種更好呢? 沒有更好只有更合適 像預設的滾動條的話,他能讓你方便摁著往下滑動(他比較寬)特別省勁,不用擔心美化過後變細 ...
  • # vane 寫這個的初衷是因為每次用node寫介面的時候總是需要一些寫大一堆的東西, 也有些人把很多介面都放在一個js文件內, 看起來很是雜亂, 後來用到nuxt寫的時候, 感覺用文件名來命名介面路徑很是方便, 無論是query參數還是params參數,都可以通過文件名來命名, 也可以通過文件夾層 ...
  • * 環境變數問題 ```typescript datasource db { provider = "mysql" url = env("DATABASE_URL") } ``` 1. `npx prisma db push` 預設取 .env 配置文件,那多環境怎麼處理? 2. 增加 `.env. ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...