流程圖渲染方式: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 8、WPF、Prism.DryIoc、MVVM設計模式、Blazor以及MySQL資料庫構建的企業級工作流系統的WPF客戶端框架-AIStudio.Wpf.AClient 6.0。 項目介紹 框架採用了 Prism 框架來實現 MVVM 模式,不僅簡化了 MVVM 的典型 ...
  • 先看一下效果吧: 我們直接通過改造一下原版的TreeView來實現上面這個效果 我們先創建一個普通的TreeView 代碼很簡單: <TreeView> <TreeViewItem Header="人事部"/> <TreeViewItem Header="技術部"> <TreeViewItem He ...
  • 1. 生成式 AI 簡介 https://imp.i384100.net/LXYmq3 2. Python 語言 https://imp.i384100.net/5gmXXo 3. 統計和 R https://youtu.be/ANMuuq502rE?si=hw9GT6JVzMhRvBbF 4. 數 ...
  • 本文為大家介紹下.NET解壓/壓縮zip文件。雖然解壓縮不是啥核心技術,但壓縮性能以及進度處理還是需要關註下,針對使用較多的zip開源組件驗證,給大家提供個技術選型參考 之前在《.NET WebSocket高併發通信阻塞問題 - 唐宋元明清2188 - 博客園 (cnblogs.com)》講過,團隊 ...
  • 之前寫過兩篇關於Roslyn源生成器生成源代碼的用例,今天使用Roslyn的代碼修複器CodeFixProvider實現一個cs文件頭部註釋的功能, 代碼修複器會同時涉及到CodeFixProvider和DiagnosticAnalyzer, 實現FileHeaderAnalyzer 首先我們知道修 ...
  • 在軟體行業,經常會聽到一句話“文不如表,表不如圖”說明瞭圖形在軟體應用中的重要性。同樣在WPF開發中,為了程式美觀或者業務需要,經常會用到各種個樣的圖形。今天以一些簡單的小例子,簡述WPF開發中幾何圖形(Geometry)相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 在 C# 中使用 RabbitMQ 通過簡訊發送重置後的密碼到用戶的手機號上,你可以按照以下步驟進行 1.安裝 RabbitMQ 客戶端庫 首先,確保你已經安裝了 RabbitMQ 客戶端庫。你可以通過 NuGet 包管理器來安裝: dotnet add package RabbitMQ.Clien ...
  • 1.下載 Protocol Buffers 編譯器(protoc) 前往 Protocol Buffers GitHub Releases 頁面。在 "Assets" 下找到適合您系統的壓縮文件,通常為 protoc-{version}-win32.zip 或 protoc-{version}-wi ...
  • 簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...