流程圖渲染方式:Canvas vs SVG

来源:https://www.cnblogs.com/dtux/p/18272816
-Advertisement-
Play Games

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 本文作者:霽明 背景 我們產品中會有一些流程圖應用,例如審批中心的審批流程圖: 我們數棧產品內的流程圖,基本都是使用的 mxGraph 實現的,mxGraph 使用了S ...


我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。

本文作者:霽明

背景

我們產品中會有一些流程圖應用,例如審批中心的審批流程圖:

file

我們數棧產品內的流程圖,基本都是使用的 mxGraph 實現的,mxGraph 使用了SVG來渲染圖形。
流程圖組件庫除了 mxGraph,還有其他一些流行的庫,例如:ReactFlow、G6、X6等等,各個庫的特點、具體實現原理各有不同,但圖形渲染方式卻主要都是這兩種:Canvas 和 SVG。
本文會通過繪製流程圖(只是簡單繪製,不涉及圖表庫的實現),來介紹 Canvas 和 SVG 的使用方式、動畫實現以及兩者之間的一些差異。

Canvas

簡介

MDN 對 Canvas 的介紹:

Canvas API 提供了一個通過 JavaScript 和 HTML的 <canvas>元素來繪製圖形的方式。它可以用於動畫、游戲畫面、數據可視化、圖片編輯以及實時視頻處理等方面。

目前所有主流的瀏覽器都支持 Canvas。

使用

基本用法

創建藍白紅3個色塊:

import { useEffect } from 'react';

function Page() {
  useEffect(() => {
    const canvas = document.getElementById('canvas') as HTMLCanvasElement;
    if (canvas?.getContext) {
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = '#002153';
      ctx.fillRect(10, 10, 50, 100);
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(60, 10, 50, 100);
      ctx.fillStyle = '#d00922';
      ctx.fillRect(110, 10, 50, 100);
    }
  }, []);
  return <canvas id="canvas"></canvas>;
}

export default Page;

效果如下圖:

file

繪製流程圖

繪製一個開始節點、一個中間節點和一個結束節點,節點之間用有向線條進行連接,如下圖:

file

前置知識:
devicePixelRatio:設備像素比,返回當前顯示設備的物理像素解析度與 _CSS _ 像素解析度之比,它告訴瀏覽器應使用多少屏幕實際像素來繪製單個 CSS 像素。比如屏幕物理像素是2000px,css 像素是1000px,則設備像素比為2。

實現代碼如下:

import { useEffect } from 'react';
import styles from '../../styles/canvas.module.css';

function Page() {
  useEffect(() => {
    const canvas = document.getElementById('canvas') as HTMLCanvasElement;
    if (canvas?.getContext) {
      // 處理圖像模糊問題
      const ratio = window.devicePixelRatio || 1;
      const { width, height } = canvas;
      canvas.width = Math.round(width * ratio);
      canvas.height = Math.round(height * ratio);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;

      const ctx = canvas.getContext('2d');
      // 放大(處理圖像模糊問題)
      ctx.scale(ratio, ratio);
      ctx.font = '12px sans-serif';

      // 開始節點
      ctx.beginPath();
      ctx.arc(300, 125, 25, Math.PI / 2, (Math.PI * 3) / 2, false); // 左邊框
      ctx.lineTo(350, 100);
      ctx.arc(350, 125, 25, (Math.PI * 3) / 2, (Math.PI * 5) / 2, false); // 右邊框
      ctx.lineTo(300, 150);
      ctx.lineWidth = 3;
      ctx.stroke();
      ctx.fillStyle = '#FFF';
      ctx.fill();
      ctx.fillStyle = '#000';
      ctx.fillText('開始', 312, 130);

      // 中間節點
      ctx.beginPath();
      ctx.arc(280, 230, 5, Math.PI, (Math.PI * 3) / 2, false); // 左上圓角
      ctx.lineTo(370, 225);
      ctx.arc(370, 230, 5, (Math.PI * 3) / 2, Math.PI * 2, false); // 右上圓角
      ctx.lineTo(375, 270);
      ctx.arc(370, 270, 5, 0, Math.PI / 2, false); // 右下圓角
      ctx.lineTo(280, 275);
      ctx.arc(280, 270, 5, Math.PI / 2, Math.PI, false); // 左下圓角
      ctx.lineTo(275, 230);
      ctx.lineWidth = 3;
      ctx.stroke();
      ctx.fillStyle = '#FFF';
      ctx.fill();
      ctx.fillStyle = '#000';
      ctx.fillText('中間節點', 300, 254);

      // 結束節點
      ctx.beginPath();
      ctx.arc(300, 400, 25, Math.PI / 2, (Math.PI * 3) / 2, false); // 左邊框
      ctx.lineTo(350, 375);
      ctx.arc(350, 400, 25, (Math.PI * 3) / 2, (Math.PI * 5) / 2, false); // 右邊框
      ctx.lineTo(300, 425);
      ctx.stroke();
      ctx.fillStyle = '#FFF';
      ctx.fill();
      ctx.fillStyle = '#000';
      ctx.fillText('結束', 312, 405);

      // 線條1
      ctx.beginPath();
      ctx.moveTo(325, 150);
      ctx.lineTo(325, 225);
      ctx.lineWidth = 1;
      ctx.stroke();
      // 箭頭1
      ctx.beginPath();
      ctx.moveTo(320, 215);
      ctx.lineTo(330, 215);
      ctx.lineTo(325, 225);
      ctx.fill();

      // 線條2
      ctx.beginPath();
      ctx.moveTo(325, 275);
      ctx.lineTo(325, 375);
      ctx.stroke();
      // 箭頭2
      ctx.beginPath();
      ctx.moveTo(320, 365);
      ctx.lineTo(330, 365);
      ctx.lineTo(325, 375);
      ctx.fill();
    }
  }, []);
  return (
    <div className={styles.container}>
      <canvas id="canvas" width="800" height="600"></canvas>
    </div>
  );
}

export default Page;

繪製圖形可以通過繪製矩形、繪製路徑的方式來繪製圖形,還可以使用 Path2D 對象來繪製,具體使用方法可以查看MDN

樣式和顏色

給節點加上樣式,效果如下:

file

對比上一步,可以發現給節點內容和邊框填充了顏色,以開始節點為例:

...

// 開始節點
ctx.beginPath();
ctx.arc(300, 125, 25, Math.PI / 2, (Math.PI * 3) / 2, false); // 左邊框
ctx.lineTo(350, 100);
ctx.arc(350, 125, 25, (Math.PI * 3) / 2, (Math.PI * 5) / 2, false); // 右邊框
ctx.lineTo(300, 150);
ctx.lineWidth = 3;
ctx.strokeStyle = '#82b366';
ctx.stroke();
ctx.fillStyle = '#d5e8d4';
ctx.fill();
ctx.fillStyle = '#000';
ctx.fillText('開始', 312, 130);

...

canvas 支持繪製許多樣式,例如:顏色、透明度、線條樣式、陰影等,具體使用可查看 MDN

動畫實現

實現線條流動動畫,實現效果如下圖所示:

file

實現原理:將線條設置為虛線,然後設置偏移量,每間隔一定時間渲染一次,每次的偏移量都遞增,便實現了線條流動的動畫效果。
原理瞭解了,但在開發之前有兩個點要考慮一下:

  • 動畫是有執行頻率的,要控制的話用哪種方式好一點?
  • 每次動畫執行時,一般是整個畫布都刷新,考慮到性能問題,是否可以局部刷新?

帶著這兩個問題,我們看下代碼實現:

import { useEffect } from 'react';
import styles from '../../styles/page.module.css';

const rAFSetInterval = (handler: (timer: number) => void, timeout?: number) => {
  let timer = null;
  let startTime = Date.now();
  const loop = () => {
    let currentTime = Date.now();
    if (currentTime - startTime >= timeout) {
      startTime = currentTime;
      handler(timer);
    }
    timer = requestAnimationFrame(loop);
  };
  loop();
  return timer;
};

function Page() {
  let canvas: HTMLCanvasElement;
  let ctx: CanvasRenderingContext2D;
  let offset = 0;

  useEffect(() => {
    canvas = document.getElementById('canvas') as HTMLCanvasElement;
    if (canvas) {
      const ratio = window.devicePixelRatio || 1;
      const { width, height } = canvas;
      canvas.width = Math.round(width * ratio);
      canvas.height = Math.round(height * ratio);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx = canvas.getContext('2d');
      ctx.scale(ratio, ratio);
      ctx.font = '12px sans-serif';
      draw();
      rAFSetInterval(run, 50);
    }
  }, []);

  const run = () => {
    offset++;
    if (offset > 1000) {
      offset = 0;
    }
    drawAnimateLine();
  };

  const draw = () => {
    // 初始化
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.setLineDash([]);
    ctx.lineDashOffset = 0;

    // 開始節點
    ctx.beginPath();
    ctx.arc(300, 125, 25, Math.PI / 2, (Math.PI * 3) / 2, false); // 左邊框
    ctx.lineTo(350, 100);
    ctx.arc(350, 125, 25, (Math.PI * 3) / 2, (Math.PI * 5) / 2, false); // 右邊框
    ctx.lineTo(300, 150);
    ctx.lineWidth = 3;
    ctx.strokeStyle = '#82b366';
    ctx.stroke();
    ctx.fillStyle = '#d5e8d4';
    ctx.fill();
    ctx.fillStyle = '#000';
    ctx.fillText('開始', 312, 130);

    // 中間節點
    ctx.beginPath();
    ctx.arc(280, 230, 5, Math.PI, (Math.PI * 3) / 2, false); // 左上圓角
    ctx.lineTo(370, 225);
    ctx.arc(370, 230, 5, (Math.PI * 3) / 2, Math.PI * 2, false); // 右上圓角
    ctx.lineTo(375, 270);
    ctx.arc(370, 270, 5, 0, Math.PI / 2, false); // 右下圓角
    ctx.lineTo(280, 275);
    ctx.arc(280, 270, 5, Math.PI / 2, Math.PI, false); // 左下圓角
    ctx.lineTo(275, 230);
    ctx.lineWidth = 3;
    ctx.strokeStyle = '#6c8ebf';
    ctx.stroke();
    ctx.fillStyle = '#dae8fc';
    ctx.fill();
    ctx.fillStyle = '#000';
    ctx.fillText('中間節點', 300, 254);

    // 結束節點
    ctx.beginPath();
    ctx.arc(300, 375, 25, Math.PI / 2, (Math.PI * 3) / 2, false); // 左邊框
    ctx.lineTo(350, 350);
    ctx.arc(350, 375, 25, (Math.PI * 3) / 2, (Math.PI * 5) / 2, false); // 右邊框
    ctx.lineTo(300, 400);
    ctx.strokeStyle = '#82b366';
    ctx.stroke();
    ctx.fillStyle = '#d5e8d4';
    ctx.fill();
    ctx.fillStyle = '#000';
    ctx.fillText('結束', 312, 380);

    // 線條1
    ctx.beginPath();
    ctx.moveTo(325, 150);
    ctx.lineTo(325, 223);
    ctx.setLineDash([4, 4]);
    ctx.lineDashOffset = -offset;
    ctx.lineWidth = 1.5;
    ctx.strokeStyle = '#000';
    ctx.stroke();
    // 箭頭1
    ctx.beginPath();
    ctx.moveTo(320, 215);
    ctx.lineTo(325, 218);
    ctx.lineTo(330, 215);
    ctx.lineTo(325, 225);
    ctx.fill();

    // 線條2
    ctx.beginPath();
    ctx.moveTo(325, 275);
    ctx.lineTo(325, 348);
    ctx.stroke();
    // 箭頭2
    ctx.beginPath();
    ctx.moveTo(320, 340);
    ctx.lineTo(325, 343);
    ctx.lineTo(330, 340);
    ctx.lineTo(325, 350);
    ctx.fill();
  };

  const drawAnimateLine = () => {
    // 清空線條
    ctx.clearRect(324, 150, 2, 67);
    ctx.clearRect(324, 275, 2, 67);

    // 繪製線條1
    ctx.beginPath();
    ctx.moveTo(325, 150);
    ctx.lineTo(325, 223);
    ctx.setLineDash([4, 4]);
    ctx.lineDashOffset = -offset;
    ctx.lineWidth = 1.5;
    ctx.strokeStyle = '#000';
    ctx.stroke();

    // 繪製線條2
    ctx.beginPath();
    ctx.moveTo(325, 275);
    ctx.lineTo(325, 348);
    ctx.stroke();
  };

  return (
    <div className={styles.container}>
      <canvas id="canvas" width="800" height="600"></canvas>
    </div>
  );
}

export default Page;

針對前面的兩個問題,這裡總結一下:

  • 使用 requestAnimationFrame 實現一個 setInterval 方法,做到定時控制和性能兼顧
  • 針對動畫區域,通過坐標和區域寬高,進行 canvas 的局部刷新

SVG

簡介

引用 MDN 對 SVG 的介紹:

可縮放矢量圖形(Scalable Vector Graphics,SVG)基於 XML 標記語言,用於描述二維的矢量圖形。
和傳統的點陣圖像模式(如 JPEG 和 PNG)不同的是,SVG 格式提供的是矢量圖,這意味著它的圖像能夠被無限放大而不失真或降低質量,並且可以方便地修改內容,無需圖形編輯器。通過使用合適的庫進行配合,SVG 文件甚至可以隨時進行本地化。

目前所有主流的瀏覽器都支持SVG(IE部分支持)。

使用

常用標簽

流程圖中主要用到的幾種標簽:
<svg>
SVG 容器元素,SVG 的代碼都包裹在該元素下,可以作為根元素(一般是 svg 圖片),也可以內嵌在HTML文檔中。如果 svg 不是根元素,svg 元素可以用於在當前文檔內嵌套一個獨立的 svg 片段。這個獨立片段擁有獨立的視口和坐標系統。
<g>
元素 g 是用來組合對象的容器。添加到 g 元素上的變換會應用到其所有的子元素上。添加到 g 元素的屬性會被其所有的子元素繼承。
<rect>
rect元素是 SVG 的一個基本形狀,用來創建矩形,基於一個角位置以及它的寬和高。它還可以用來創建圓角矩形。
<path>
path 元素是用來定義形狀的通用元素。所有的基本形狀都可以用 path 元素來創建。
<foreignObject>
foreignObject 元素允許包含來自不同的 XML 命名空間的元素。在瀏覽器的上下文中,很可能是 XHTML / HTML。在我們的流程圖中,通過 HTML 渲染的節點一般都渲染在這個標簽內。

基本用法

使用svg渲染圖片

function Page() {
  return (
    <svg width="150" height="100">
      <rect width="50" height="100" x="0" fill="#002153" />
      <rect width="50" height="100" x="50" fill="#ffffff" />
      <rect width="50" height="100" x="100" fill="#d00922" />
    </svg>
  );
}

export default Page;

上面代碼渲染效果如下圖:

file

繪製流程圖

使用svg繪製流程圖:

file

代碼實現如下:

import styles from '../../styles/page.module.css';

function Page() {
  return (
    <svg width="800" height="600" className={styles.container}>
      <g>
        <path
          d="M 320 110 C 286 110, 286 160, 320 160 L 370 160 C 404 160, 404 110, 370 110 Z"
          stroke="#82b366"
          strokeWidth="2"
          fill="#d5e8d4"
          />
        <text x="332" y="140" style={{ fontSize: 12 }}>
          開始
        </text>
      </g>
      <g>
        <rect
          x="295"
          y="235"
          width="100"
          height="50"
          rx="5"
          fill="#dae8fc"
          stroke="#6c8ebf"
          strokeWidth="2"
          ></rect>
        <text x="320" y="264" style={{ fontSize: 12 }}>
          中間節點
        </text>
      </g>
      <g>
        <path
          d="M 320 360 C 286 360, 286 410, 320 410 L 370 410 C 404 410, 404 360, 370 360 Z"
          stroke="#82b366"
          strokeWidth="2"
          fill="#d5e8d4"
          />
        <text x="332" y="390" style={{ fontSize: 12 }}>
          結束
        </text>
      </g>
      <g>
        <path d="M 345 160 L 345 235" stroke="#000"></path>
        <path d="M 340 225 L 345 228 L 350 225 L 345 235 Z" fill="#000"></path>
      </g>
      <g>
        <path d="M 345 285 L 345 360" stroke="#000"></path>
        <path d="M 340 350 L 345 353 L 350 350 L 345 360 Z" fill="#000"></path>
      </g>
    </svg>
  );
}

export default Page;

以開始節點為例,主要看下path元素:

<path
  d="M 320 110 C 286 110, 286 160, 320 160 L 370 160 C 404 160, 404 110, 370 110 Z"
  stroke="#82b366"
  strokeWidth="2"
  fill="#d5e8d4"
/>

d 屬性定義了要繪製的路徑,路徑定義是一個路徑命令組成的列表,其中的每一個命令由命令字母和用於表示命令參數的數字組成。每個命令之間通過空格或逗號分隔。
M 表示 move to,即移動到某個坐標;L 表示 line to,即連線到某個坐標。
C 表示使用三次方貝塞爾曲線,後面跟隨3個坐標點,分別是起始控制點、終點控制點、終點。
Z 表示 ClosePath,將從當前位置繪製一條直線到路徑中的第一個點。上面只用到了4種命令,而命令總共有20種,具體可以查看MDN
stroke、strokeWidth、fill 則分別指定了邊框顏色、寬度,以及填充顏色。

動畫實現

實現線條流動動畫,實現效果如下圖:

file

實現原理:先將線條設置為虛線,然後通過 css 動畫,修改虛線的偏移量並無限迴圈,從而實現線條流動效果。
代碼實現如下:

.animate-path {
  stroke-dasharray: 5;
  animation: dashdraw 0.5s linear infinite;
}
@keyframes dashdraw {
  0% {
    stroke-dashoffset: 10;
  }
}

svg 可以通過 css、js 或者 animate 標簽來實現動畫,適用於需要高質量矢量圖形、可縮放和交互性強的場景

對比

使用方式

Canvas 是比 SVG 更低級別的 API,繪製圖形需要通過 JS 來操作。Canvas 提供了更大的靈活性,但複雜度也更高,理論上任何使用 SVG 繪製的圖形,都可以通過 Canvas繪製出來。相反,由於 SVG 是比 Canvas 更高級別的 API,可以當作 HTML 元素去使用,也可以結合 JS、CSS 去操作,使用 SVG 創建一些複雜的圖形會比使用 Canvas 更加簡單。

交互性

SVG 位於 DOM 中,和普通 DOM 元素一樣支持響應事件。Canvas 也可以響應交互事件,但需要額外的代碼去實現。

性能

Canvas 和 SVG 性能的影響因素主要有兩個:繪製圖形的數量、繪製圖形的大小。
下圖是微軟 MSDN 上給的一個對比圖。

file

Canvas 的性能受畫布尺寸影響更大,而 SVG 的性能受圖形元素個數影響更大。網路上的對於性能及使用相關的建議是:如果繪製圖像面積大或者繪製元素數量小時,建議使用SVG,如果繪製圖像面積小或者繪製元素數量較大時,則建議使用 Canvas。

總結

本文介紹了 Canvas 和 SVG 的一些基本概念和使用方式,在我們日常開發中,有時會碰到需要繪製圖形的場景,對於 Canvas 和 SVG,分別有其適合的場景:

  • 需要繪製的圖像簡單、交互性強或者是矢量圖(例如圖標),建議使用 SVG。
  • 需要支持像素級別的操作,或者複雜的動畫和交互(例如數據可視化、互動式游戲),建議使用Canvas。

大多數流程圖組件庫都是使用 Canvas 或 SVG 來繪製圖形,流程圖一般圖形簡單,節點數量不多,會有一些簡單的交互,因而大多數流程圖組件庫都使用 SVG 來進行渲染,例如 ReactFlow、draw.io、mxGraph、X6、XFlow 等,都是使用 svg 來進行渲染。

鏈接

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
https://developer.mozilla.org/zh-CN/docs/Web/SVG

最後

歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧 UED 團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎 star


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

-Advertisement-
Play Games
更多相關文章
  • ★ 背景說明 在瀏覽器環境中,由於安全性限制,不能直接將網路圖片地址轉換成 File 對象。File 對象是用戶在客戶端上傳文件時才能創建的,而不能由前端代碼直接將網路圖片地址轉換成 File 對象。 ★ 解決方案 如果你想要將網路圖片地址轉換成 Fie 對象,你需要先將圖片下載到客戶端,然後再將其 ...
  • 腳本化CSS 我們剛講過如何獲取和設置行內樣式的值,但是我們開發不會所有樣式都寫在行內,同時js沒法獲取內嵌樣式表和外部樣式表中的值. 事實上DOM提供了可靠的API,得到計算後的樣式。 1. 獲取計算樣式表 只讀,不可寫 獲取的值是計算後的絕對值,不是相對值 window.getComputedS ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 場景是用戶通過微信掃app內的收款碼,跳到一個h5頁面。然後完成支付。 代碼實現的整體流程: 使用微信掃碼,碼是app內生成的,碼的內容是一個h5頁面鏈接,掃碼完成後跳轉到自定義的h5支付界面。 掃碼進入後,將頁面展示所需要的參數進行緩存起來, ...
  • 摘要:本文詳細介紹了Nuxt3中的六個核心生命周期鉤子及其用法,包括build:done、build:manifest、builder:generateApp、builder:watch、pages:extend和server:devHandler:handler。內容涵蓋各鉤子的調用時機、參數、環... ...
  • 一.定時器 1. JS存在兩種定時器 setTimeout() 延遲定時器 setInterval() 迴圈定時器(“間隔器”) 定時器中的函數掛載在window對象,內部的this ——> window setTimerout(function(){ console.log('wuwei') }, ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 近期產品期望在後臺發佈帖子或視頻時,需要添加 @用戶 的功能,以便用戶收到通知,例如“xxx在xxx提及了您!”。然而,現有的開源庫未能滿足我們的需求,例如 ant-design 的 Mentions 組件: 但是不難發現跟微信飛書對比下,有兩 ...
  • 前言 petite-vue 是為漸進增強而優化的另一種 Vue 發行版。它提供與標準 Vue 相同的模板語法和反應性心智模型。 不過,它專門針對在由伺服器框架呈現的現有 HTML 頁面上“散佈”少量交互進行了優化。 petite-vue,它在提供 vue 基本功能的同時,還能一個輕量級,簡單應用的微 ...
  • 概述了Nuxt3的六個關鍵生命周期鉤子用途:modules:before至build:before,指導如何在應用初始化、模塊管理、配置解析、模板處理及構建前執行自定義操作,附帶實例代碼,強化Nuxt應用的靈活性和可控性。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...