<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【總結】1796- 原生 canvas 如何實現(xiàn)大屏?

          共 11209字,需瀏覽 23分鐘

           ·

          2023-09-06 17:39


          作者: 小丑依然是我

          https://juejin.cn/post/7165564571128692773

          前言

          可視化大屏該如何做?有可能一天完成嗎?廢話不多說,直接看效果,

          線上 Demo 地址:

          https://lxfu1.github.io/large-screen-visualization/

          看完這篇文章(這個項目),你將收獲:

          1. 全局狀態(tài)真的很簡單,你只需 5 分鐘就能上手
          2. 如何緩存函數(shù),當入?yún)⒉蛔儠r,直接使用緩存值
          3. 千萬節(jié)點的圖如何分片渲染,不卡頓頁面操作
          4. 項目單測該如何寫?
          5. 如何用 canvas 繪制各種圖表,如何實現(xiàn) canvas 動畫
          6. 如何自動化部署自己的大屏網(wǎng)站

          實現(xiàn)

          項目基于 Create React App[1] --template typescript搭建,包管理工具使用的 pnpm ,pnpm 的優(yōu)勢這里不多介紹(快+節(jié)省磁盤空間)。

          由于項目 package.json 里面有限制包版本(最新版本的 G6 會導致 OOM,官方短時間能應該會修復),如果使用的 yarn 或 npm 的話,改為對應的 resolutions 即可。

           "pnpm": {
              "overrides": {
                "@antv/g6""4.7.10"
              }
            }

          "resolutions": {
            "@antv/g6""4.7.10"
          },

          啟動

          1. clone項目
          git clone https://github.com/lxfu1/large-screen-visualization.git

          1. pnpm 安裝 npm install -g pnpm
          2. 啟動:pnpm start 即可,建議配置 alias ,可以簡化各種命令的簡寫 eg:p start,不出意外的話,你可以通過 http://localhost:3000/ 訪問了
          3. 測試:p test
          4. 構建:p build

          強烈建議大家先 clone 項目!

          分析

          全局狀態(tài)

          全局狀態(tài)用的 valtio[2] ,位于項目 src/models目錄下,強烈推薦。

          優(yōu)點:數(shù)據(jù)與視圖分離的心智模型,不再需要在 React 組件或 hooks 里用 useState 和 useReducer 定義數(shù)據(jù),或者在 useEffect 里發(fā)送初始化請求,或者考慮用 context 還是 props 傳遞數(shù)據(jù)。

          缺點:兼容性,基于 proxy 開發(fā),對低版本瀏覽器不友好,當然,大屏應該也不會考慮 IE 這類瀏覽器。

          import { proxy } from "valtio";
          import { NodeConfig } from "@ant-design/graphs";

          type IState = {
            sliderWidth: number;
            sliderHeight: number;
            selected: NodeConfig | null;
          };

          export const state: IState = proxy({
            sliderWidth: 0,
            sliderHeight: 0,
            selected: null,
          });

          狀態(tài)更新:

          import { state } from "src/models";

          state.selected = e.item?.getModel() as NodeConfig;

          狀態(tài)消費:

          import { useSnapshot } from "valtio";
          import { state } from "src/models";

          export const BarComponent = () => {
            const snap = useSnapshot(state);

            console.log(snap.selected)
          }

          當我們選中圖譜節(jié)點的時候,由于 BarComponent 組件監(jiān)聽了 selected 狀態(tài),所以該組件會進行更新。有沒有感覺非常簡單?一些高級用法建議大家去官網(wǎng)查看,不再展開。

          函數(shù)緩存

          為什么需要函數(shù)緩存?當然,在這個項目中函數(shù)緩存比較雞肋,為了用而用,試想,如果有一個函數(shù)計算量非常大,組件內又有多個 state 頻繁更新,怎么確保函數(shù)不被重復調用呢?

          可能大家會想到 useMemo``useCallback等手段,這里要介紹的是 React 官方的 cache 方法,已經(jīng)在 React 內部使用,但未暴露。實現(xiàn)上借鑒(抄襲)ReactCache[3],通過緩存的函數(shù) fn 及其參數(shù)列表來構建一個 cacheNode 鏈表,然后基于鏈表最后一項的狀態(tài)來作為函數(shù) fn 與該組參數(shù)的計算緩存結果。

          代碼位于 src/utils/cache

          interface CacheNode {
          /**
          * 節(jié)點狀態(tài)
          * - 0:未執(zhí)行
          * - 1:已執(zhí)行
          * - 2:出錯
          */
          s: 0 | 1 | 2;
          // 緩存值
          v: unknown;
          // 特殊類型(object,fn),使用 weakMap 存儲,避免內存泄露
          o: WeakMap<Function | object, CacheNode> | null;
          // 基本類型
          p: Map<Function | object, CacheNode> | null;
          }

          const cacheContainer = new WeakMap<Function, CacheNode>();

          export const cache = (fn: Function): Function => {
          const UNTERMINATED = 0;
          const TERMINATED = 1;
          const ERRORED = 2;

          const createCacheNode = (): CacheNode => {
          return {
          s: UNTERMINATED,
          v: undefined,
          o: null,
          p: null,
          };
          };

          return function () {
          let cacheNode = cacheContainer.get(fn);
          if (!cacheNode) {
          cacheNode = createCacheNode();
          cacheContainer.set(fn, cacheNode);
          }
          for (let i = 0; i < arguments.length; i++) {
          const arg = arguments[i];
          // 使用 weakMap 存儲,避免內存泄露
          if (
          typeof arg === "function" ||
          (typeof arg === "object" && arg !== null)
          ) {
          let objectCache: CacheNode["o"] = cacheNode.o;
          if (objectCache === null) {
          objectCache = cacheNode.o = new WeakMap();
          }
          let objectNode = objectCache.get(arg);
          if (objectNode === undefined) {
          cacheNode = createCacheNode();
          objectCache.set(arg, cacheNode);
          } else {
          cacheNode = objectNode;
          }
          } else {
          let primitiveCache: CacheNode["p"] = cacheNode.p;
          if (primitiveCache === null) {
          primitiveCache = cacheNode.p = new Map();
          }
          let primitiveNode = primitiveCache.get(arg);
          if (primitiveNode === undefined) {
          cacheNode = createCacheNode();
          primitiveCache.set(arg, cacheNode);
          } else {
          cacheNode = primitiveNode;
          }
          }
          }
          if (cacheNode.s === TERMINATED) return cacheNode.v;
          if (cacheNode.s === ERRORED) {
          throw cacheNode.v;
          }
          try {
          const res = fn.apply(null, arguments as any);
          cacheNode.v = res;
          cacheNode.s = TERMINATED;
          return res;
          } catch (err) {
          cacheNode.v = err;
          cacheNode.s = ERRORED;
          throw err;
          }
          };
          };

          如何驗證呢?我們可以簡單看下單測,位于src/__tests__/utils/cache.test.ts

          import { cache } from "src/utils";

          describe("cache", () => {
          const primitivefn = jest.fn((a, b, c) => {
          return a + b + c;
          });

          it("primitive", () => {
          const cacheFn = cache(primitivefn);
          const res1 = cacheFn(1, 2, 3);
          const res2 = cacheFn(1, 2, 3);
          expect(res1).toBe(res2);
          expect(primitivefn).toBeCalledTimes(1);
          });
          });

          可以看出,即使我們調用了 2 次 cacheFn,由于入?yún)⒉蛔?,fn 只被執(zhí)行了一次,第二次直接返回了第一次的結果。

          項目里面在做 circle 動畫的時候使用了,因為該動畫是繞圓周無限循環(huán)的,當循環(huán)過一周之后,后的動畫和之前的完全一致,沒必要再次計算對應的 circle 坐標,所以我們使用了 cache ,位于src/components/background/index.tsx。

            const cacheGetPoint = cache(getPoint);
          let p = 0;
          const animate = () => {
          if (p >= 1) p = 0;
          const { x, y } = cacheGetPoint(p);
          ctx.clearRect(0, 0, 2 * clearR, 2 * clearR);
          createCircle(aCtx, x, y, circleR, "#fff", 6);
          p += 0.001;
          requestAnimationFrame(animate);
          };
          animate();

          分片渲染

          你有審查元素嗎?項目背景圖是通過 canvas 繪制的,并不是背景圖片!通過 canvas 繪制如此多的小圓點,會不會阻礙頁面操作呢?

          當數(shù)據(jù)量足夠大的時候,是會阻礙的,大家可以把 NodeMargin 設置為 0.1 ,同時把 schduler 調用去掉,直接改為同步繪制。當節(jié)點數(shù)量在 500 W 的時候,如果沒有開啟切片,頁面白屏時間在 MacBook Pro M1 上白屏時間大概是 8.5 S;開啟分片渲染時頁面不會出現(xiàn)白屏,而是從左到右逐步繪制背景圖,每個任務的執(zhí)行時間在 16S 左右波動。

            const schduler = (tasks: Function[]) => {
          const DEFAULT_RUNTIME = 16;
          const { port1, port2 } = new MessageChannel();
          let isAbort = false;

          const promise: Promise<any> = new Promise((resolve, reject) => {
          const runner = () => {
          const preTime = performance.now();
          if (isAbort) {
          return reject();
          }
          do {
          if (tasks.length === 0) {
          return resolve([]);
          }
          const task = tasks.shift();
          task?.();
          } while (performance.now() - preTime < DEFAULT_RUNTIME);
          port2.postMessage("");
          };
          port1.onmessage = () => {
          runner();
          };
          });
          // @ts-ignore
          promise.abort = () => {
          isAbort = true;
          };
          port2.postMessage("");
          return promise;
          };

          分片渲染可以不阻礙用戶操作,但延遲了任務的整體時長,是否開啟還是取決于數(shù)據(jù)量。如果每個分片實際執(zhí)行時間大于 16ms 也會造成阻塞,并且會堆積,并且任務執(zhí)行的時候沒有等,最終渲染狀態(tài)和預期不一致,所以 task 的拆分也很重要。

          單測

          這里不想多說,大家可以運行 pnpm test看看效果,環(huán)境已經(jīng)搭建好;由于項目里面用到了 canvas 所以需要 mock 一些環(huán)境,這里的 mock 可以理解為“我們前端代碼跑在瀏覽器里運行,依賴了瀏覽器環(huán)境以及對應的 API,但由于單測沒有跑在瀏覽器里面,所以需要 mock 瀏覽器環(huán)境”,例如項目里面設置的 jsdom、jest-canvas-mock 以及 worker 等,更多推薦直接訪問 jest[4] 官網(wǎng)。

          // jest-dom adds custom jest matchers for asserting on DOM nodes.
          import "@testing-library/jest-dom";

          Object.defineProperty(URL, "createObjectURL", {
            writable: true,
            value: jest.fn(),
          });

          class Worker {
            onmessage: () => void;
            url: string;
            constructor(stringUrl) {
              this.url = stringUrl;
              this.onmessage = () => {};
            }

            postMessage() {
              this.onmessage();
            }
            terminate() {}
            onmessageerror() {}
            addEventListener() {}
            removeEventListener() {}
            dispatchEvent(): boolean {
              return true;
            }
            onerror() {}
          }
          window.Worker = Worker;

          自動化部署

          開發(fā)過項目的同學都知道,前端編寫的代碼最終是要進行部署的,目前比較流行的是前后端分離,前端獨立部署,通過 proxy 的方式請求后端服務;或者是將前端構建產(chǎn)物推到后端服務上,和后端一起部署。如何做自動化部署呢,對于一些不依賴后端的項目來說,我們可以借助 github 提供的 gh-pages 服務來做自動化部署,CI、CD 僅需配置對應的 actions 即可,在倉庫 settings/pages 下面選擇對應分支即可完成部署。

          例如項目里面的.github/workflows/gh-pages.yml,表示當 master 分支有代碼提交時,會執(zhí)行對應的 jobs,并借助 peaceiris/actions-gh-pages@v3將構建產(chǎn)物同步到 gh-pages 分支。

          name: github pages

          on:
            push:
              branches:
                - master # default branch
                
          env:
            CI: false
            PUBLIC_URL: '/large-screen-visualization'

          jobs:
            deploy:
              runs-on: ubuntu-latest
              steps:
                - uses: actions/checkout@v3
                - run: yarn
                - run: yarn build
                - name: Deploy
                  uses: peaceiris/actions-gh-pages@v3
                  with:
                    github_token: ${{ secrets.GITHUB_TOKEN }}
                    publish_dir: ./build

          總結

          寫文檔不易,如果看完有收獲,記得給個小星星!歡迎大家 PR!

          • Ant Design Charts[5]
          • 示例倉庫[6]

          參考資料

          [1]

          Create React App: https://create-react-app.dev/docs/getting-started

          [2]

          valtio: https://github.com/pmndrs/valtio

          [3]

          ReactCache: https://github.com/facebook/react/blob/main/packages/react/src/ReactCache.js

          [4]

          jest: https://jestjs.io/docs/26.x/getting-started

          [5]

          Ant Design Charts: https://github.com/ant-design/ant-design-charts

          [6]

          示例倉庫: https://github.com/lxfu1/large-screen-visualization



          往期回顧

          #

          如何使用 TypeScript 開發(fā) React 函數(shù)式組件?

          #

          11 個需要避免的 React 錯誤用法

          #

          6 個 Vue3 開發(fā)必備的 VSCode 插件

          #

          3 款非常實用的 Node.js 版本管理工具

          #

          6 個你必須明白 Vue3 的 ref 和 reactive 問題

          #

          6 個意想不到的 JavaScript 問題

          #

          試著換個角度理解低代碼平臺設計的本質

          回復“加群”,一起學習進步

          瀏覽 342
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费一级片免费 | 国产精品国产伦子伦露看 | 曰本国电影黄色免看费 | 夜色视频在线免费观看 | 伊人大香蕉大香蕉大香蕉 |