🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

前端圖形引擎架構設計:可擴展雙渲染引擎架構設計-支持自定義渲染器

ECS渲染引擎架構文件

《前端圖形引擎架構設計:基於ECS模式的可擴展渲染系統》
《前端圖形引擎架構設計:AI生成設計稿落地實踐》
GitHub

寫在前面

之前寫過一篇ECS文章,為什麼還要再寫一個,本質上因為之前的文檔,截至到目前為止,變化巨大,底層已經改了很多很多,所以有必要把一些內容提取出來單獨去說明。

由於字體文件較大,載入時間會比較久😞,可以把項目clone下來本地運行會比較快。

另外如果有性能問題,我會及時修復,引擎改造時間太倉促,只要不是內存洩漏,暫時沒去處理。

還有很多東西要做。

體驗地址:baiyuze.github.io/design/#/ca…

image.png
image.png
image.png

專案概覽

Duck-Core 是一個基於 ECS(Entity-Component-System)架構構建的高性能 Canvas 渲染引擎,專為複雜圖形編輯場景設計。引擎的核心特色在於雙渲染後端架構插件化系統設計極致的渲染性能優化

核心技術棧

  • CanvasKit-WASM - Google Skia 圖形庫的 WebAssembly 移植版
  • Canvas2D API - 瀏覽器原生渲染介面

架構核心亮點

  • ECS 架構模式 - 數據驅動的實體元件系統,實現邏輯與數據完全解耦
  • 雙引擎架構 - Canvas2D 與 CanvasKit 雙渲染後端,運行時無縫切換
  • 插件化設計 - 開放式擴展點,支持自定義渲染器、系統和元件
  • 極致性能 - 顏色編碼拾取、離屏渲染、渲染節流等多重優化

整體架構設計

整個引擎採用分層架構,從底層的渲染抽象到頂層的用戶互動,每一層職責清晰且可獨立替換。


ECS 架構深入解析

什麼是 ECS 架構?

ECS(Entity-Component-System)是一種源自遊戲引擎的設計模式,它徹底改變了傳統面向對象的繼承體系,轉而採用組合優於繼承的理念。

三大核心概念:

  1. Entity(實體) - 僅是唯一 ID,不包含任何數據和邏輯
  2. Component(元件) - 純數據結構,描述實體的屬性(如位置、顏色、大小)
  3. System(系統) - 純邏輯處理單元,操作特定元件組合的實體

ECS 架構的核心優勢

1. 極致的解耦性

傳統 OOP 中,功能通過繼承鏈緊密耦合。而 ECS 中,系統只依賴元件介面,實體的行為完全由元件組合決定。

// ❌ 傳統方式:緊耦合的繼承鏈
class Shape {
  render() { /* ... */ }
}
class DraggableShape extends Shape {
  drag() { /* ... */ }
}
class SelectableDraggableShape extends DraggableShape {
  select() { /* ... */ }
}

// ✅ ECS 方式:元件自由組合
const rect = createEntity()
addComponent(rect, Position, { x: 100, y: 100 })
addComponent(rect, Size, { width: 200, height: 150 })
addComponent(rect, Draggable, {})  // 可拖曳
addComponent(rect, Selected, {})   // 可選中
2. 強大的可擴展性

新增功能無需修改現有程式碼,只需添加新的元件和系統:

image.png

3. 天然的並行處理能力

系統之間無共享狀態,可以安全地並行執行:

// 多個系統可以同時讀取同一個元件
async function updateFrame() {
  await Promise.all([
    physicsSystem.update(),   // 讀取 Position
    renderSystem.update(),    // 讀取 Position
    collisionSystem.update(), // 讀取 Position
  ])
}
System 系統架構

系統負責處理邏輯,通過查詢 StateStore 獲取需要的元件數據:

abstract class System {
  abstract update(stateStore: StateStore): void
}

class RenderSystem extends System {
  update(stateStore: StateStore) {
    // 查詢所有擁有 Position 元件的實體
    for (const [entityId, position] of stateStore.position) {
      const size = stateStore.size.get(entityId)
      const color = stateStore.color.get(entityId)
      const type = stateStore.type.get(entityId)

      // 根據類型調用對應的渲染器
      this.renderMap.get(type)?.draw(entityId)
    }
  }
}

系統完整列表:


雙引擎架構設計

架構設計理念

不同的應用場景對渲染引擎有不同的需求:

  • 簡單場景:需要快速啟動、體積小、兼容性好
  • 複雜場景:需要高性能、豐富特效、大量圖形

傳統方案通常只支持單一渲染後端,難以兼顧兩者。本引擎採用雙引擎可切換架構,在運行時動態選擇最優渲染後端。


渲染後端對比

特性 Canvas2D CanvasKit (Skia)
啟動速度 ⚡️ 即時(0ms) 🐢 需載入 WASM(~2s)
包體積 ✅ 0 KB ⚠️ ~1.5 MB
瀏覽器兼容性 ✅ 100% ⚠️ 需支持 WASM
渲染性能 🟡 中等 🟢 優秀
複雜路徑渲染 🟡 一般 🟢 優秀
文字渲染 🟡 質量一般 🟢 亞像素級
濾鏡特效 ❌ 有限 ✅ 豐富
離屏渲染 ✅ 支持 ✅ 支持
最佳場景 簡單圖形、快速原型 複雜設計、高性能需求

RendererManager 渲染管理器

RendererManager 是雙引擎架構的核心樞紐,負責渲染器的註冊、切換和調度:

class RendererManager {
  rendererName: 'Canvas2D' | 'Canvaskit' = 'Canvaskit'

  // 渲染器映射表
  renderer: {
    rect: typeof RectRender
    ellipse: typeof EllipseRender
    text: typeof TextRender
    img: typeof ImgRender
    polygon: typeof PolygonRender
  }

  // 切換渲染後端
  setRenderer(name: 'Canvas2D' | 'Canvaskit') {
    this.rendererName = name

    if (name === 'Canvas2D') {
      this.renderer = Canvas2DRenderers
    } else {
      this.renderer = CanvaskitRenderers
    }
  }
}

渲染器切換流程:


渲染器統一介面

所有渲染器實現相同的介面,保證可替換性:

abstract class BaseRenderer extends System {
  constructor(protected engine: Engine) {
    super()
  }

  // 統一的渲染介面
  abstract draw(entityId: string): void
}

自定義渲染器擴展

引擎支持用戶自定義渲染器,只需實現 System 介面:

// 1. 創建自定義渲染器
class CustomStarRender extends System {
  draw(entityId: string) {
    const points = this.getComponent<Polygon>(entityId, 'polygon')
    const color = this.getComponent<Color>(entityId, 'color')

    // 自定義繪製邏輯
    const ctx = this.engine.ctx
    ctx.beginPath()
    points.points.forEach((p, i) => {
      i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)
    })
    ctx.closePath()
    ctx.fillStyle = color.fill
    ctx.fill()
  }
}

const customRenderMap = {
  star: CustomStarRender
}

// 2. 註冊到引擎
new RendererRegistry().register({
  "custom": customRenderMap
})

字體渲染優化

CanvasKit 需要預加載字體文件,引擎實現了字體管理器:

async function loadFonts(CanvasKit: any) {
  const fontsBase = import.meta.env?.MODE === 'production'
    ? '/design/fonts/'
    : '/fonts/'

  const [robotoFont, notoSansFont] = await Promise.all([
    fetch(`${fontsBase}Roboto-Regular.ttf`).then(r => r.arrayBuffer()),
    fetch(`${fontsBase}NotoSansSC-VariableFont_wght_2.ttf`).then(r => r.arrayBuffer()),
  ])

  const fontMgr = CanvasKit.FontMgr.FromData(robotoFont, notoSansFont)
  return fontMgr
}

// 在 CanvasKit 初始化時調用
export async function createCanvasKit() {
  const CanvasKit = await initCanvasKit()
  const FontMgr = await loadFonts(CanvasKit)
  return { CanvasKit, FontMgr }
}

引擎工廠模式

使用工廠函數創建不同配置的引擎實例:

export function createCanvasRenderer(engine: Engine) {
  // Canvas2D 引擎創建器
  const createCanvas2D = (config: DefaultConfig) => {
    const canvas = document.createElement('canvas')
    const dpr = window.devicePixelRatio || 1
    canvas.style.width = config.width + 'px'
    canvas.style.height = config.height + 'px'
    canvas.width = config.width * dpr
    canvas.height = config.height * dpr

    const ctx = canvas.getContext('2d', {
      willReadFrequently: true,
    }) as CanvasRenderingContext2D
    ctx.scale(dpr, dpr)

    config.container.appendChild(canvas)

    return { canvasDom: canvas, canvas: ctx, ctx }
  }

  // CanvasKit 引擎創建器
  const createCanvasKitSkia = async (config: DefaultConfig) => {
    const { CanvasKit, FontMgr } = await createCanvasKit()
    const canvasDom = document.createElement('canvas')
    const dpr = window.devicePixelRatio || 1

    canvasDom.style.width = config.width + 'px'
    canvasDom.style.height = config.height + 'px'
    canvasDom.width = config.width * dpr
    canvasDom.height = config.height * dpr
    canvasDom.id = 'canvasKitCanvas'

    config.container.appendChild(canvasDom)

    const surface = CanvasKit.MakeWebGLCanvasSurface('canvasKitCanvas')
    const canvas = surface!.getCanvas()

    return {
      canvasDom,
      surface,
      canvas,
      FontMgr,
      ck: CanvasKit,
    }
  }

  return {
    createCanvas2D,
    createCanvasKitSkia,
  }
}

Engine 引擎核心

Engine 類是整個渲染系統的中樞,協調所有子系統的運行:

class Engine implements EngineContext {
  camera: Camera = new Camera()
  entityManager: Entity = new Entity()
  SystemMap: Map<string, System> = new Map()
  rendererManager: RendererManager = new RendererManager()

  canvas!: Canvas  // 渲染畫布(類型取決於渲染後端)
  ctx!: CanvasRenderingContext2D
  ck!: CanvasKit

  constructor(public core: Core, rendererName?: string) {
    // 初始化渲染器
    this.rendererManager.rendererName = rendererName || 'Canvaskit'
    this.rendererManager.setRenderer(this.rendererManager.rendererName)
  }

  // 添加系統
  addSystem(system: System) {
    this.system.push(system)
    this.SystemMap.set(system.constructor.name, system)
  }

  // 獲取系統
  getSystemByName<T extends System>(name: string): T | undefined {
    return this.SystemMap.get(name) as T
  }

  // 清空畫布(適配雙引擎)
  clear() {
    const canvas = this.canvas as any
    if (canvas?.clearRect) {
      // Canvas2D 清空方式
      canvas.clearRect(0, 0, this.defaultSize.width, this.defaultSize.height)
    } else {
      // CanvasKit 清空方式
      this.canvas.clear(this.ck.WHITE)
    }
  }
}

插件化系統設計

系統即插件

引擎的所有功能都以 System 形式實現,每個 System 都是獨立的插件。這種設計帶來極高的靈活性:


核心系統詳解

1. EventSystem - 事件總線

EventSystem 是整個引擎的調度中樞,協調所有其他系統的執行:

class EventSystem extends System {
  private eventQueue: Event[] = []

  update(stateStore: StateStore) {
    // 執行系統更新順序
    this.executeSystem('InputSystem')      // 1. 捕獲輸入
    this.executeSystem('HoverSystem')      // 2. 檢測懸停
    this.executeSystem('ClickSystem')      // 3. 處理點擊
    this.executeSystem('DragSystem')       // 4. 處理拖曳
    this.executeSystem('ZoomSystem')       // 5. 處理縮放
    this.executeSystem('SelectionSystem')  // 6. 更新選擇
    this.executeSystem('PickingSystem')    // 7. 更新拾取快取
    this.executeSystem('RenderSystem')     // 8. 最後渲染
  }
}
2. RenderSystem - 渲染系統

RenderSystem 負責將實體繪製到畫布:

class RenderSystem extends System {
  private renderMap = new Map<string, BaseRenderer>()

  constructor(engine: Engine) {
    super()
    this.engine = engine
    this.initRenderMap()
  }

  // 初始化渲染器映射
  initRenderMap() {
    Object.entries(this.engine.rendererManager.renderer).forEach(
      ([type, RendererClass]) => {
        this.renderMap.set(type, new RendererClass(this.engine))
      }
    )
  }

  async update(stateStore: StateStore) {
    // 清空畫布
    this.engine.clear()

    // 應用相機變換
    this.engine.canvas.save()
    this.engine.canvas.translate(
      this.engine.camera.translateX,
      this.engine.camera.translateY
    )
    this.engine.canvas.scale(
      this.engine.camera.zoom,
      this.engine.camera.zoom
    )

    // 遍歷所有實體進行渲染
    for (const [entityId, pos] of stateStore.position) {
      this.engine.canvas.save()
      this.engine.canvas.translate(pos.x, pos.y)

      const type = stateStore.type.get(entityId)
      await this.renderMap.get(type)?.draw(entityId)

      this.engine.canvas.restore()
    }

    this.engine.canvas.restore()
  }
}

DSL 配置系統

設計目標

DSL(Domain Specific Language)模組的目標是將圖形場景序列化為 JSON 格式,實現:

  1. 場景持久化 - 保存到資料庫或本地存儲
  2. 場景傳輸 - 前後端數據交換
  3. 場景快照 - 撤銷/重做功能的基礎
  4. 模板復用 - 創建可復用的圖形模板

配置結構

interface DSLParams {
  type: 'rect' | 'ellipse' | 'text' | 'img' | 'polygon'
  id?: string
  position: { x: number; y: number }
  size?: { width: number; height: number }
  color?: { fill: string; stroke: string }
  rotation?: { value: number }
  scale?: { value: number }
  zIndex?: { value: number }
  selected?: { isSelected: boolean }
  // 形狀特定屬性
  font?: { family: string; size: number; weight: string }
  radius?: { value: number }
  polygon?: { points: Point[] }
}

DSL 解析器

class DSL {
  constructor(params: DSLParams) {
    this.type = params.type
    this.id = params.id || this.generateId()
    this.position = new Position(params.position)
    this.size = params.size ? new Size(params.size) : new Size()
    this.color = params.color ? new Color(params.color) : new Color()
    // ... 初始化其他元件
  }

  // 轉換為純數據對象
  toJSON(): DSLParams {
    return {
      type: this.type,
      id: this.id,
      position: { x: this.position.x, y: this.position.y },
      size: { width: this.size.width, height: this.size.height },
      color: { fill: this.color.fill, stroke: this.color.stroke },
      // ...
    }
  }
}

低耦合架構實踐

依賴方向

整個引擎嚴格遵循依賴倒置原則:

關鍵設計:

  • 上層依賴介面,不依賴具體實現
  • System 不直接依賴 Renderer,通過 RendererManager 解耦
  • Component 純數據,零依賴

總結

Duck-Core 前端渲染引擎通過以下設計實現了高性能、高擴展性:

核心優勢

  1. ECS架構 - 數據與邏輯完全分離,元件自由組合
  2. 雙引擎架構 - Canvas2D 與 CanvasKit 可熱切換,兼顧兼容性與性能
  3. 插件化系統 - 所有功能以 System 形式實現,按需加載
  4. 低耦合設計 - 介面隔離、依賴倒置、事件驅動
  5. 極致性能 - 渲染節流、離屏快取、視口裁剪、內存優化

原文出處:https://juejin.cn/post/7572387666979995674


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬5   ❤️5
339
🥈
我愛JS
📝2   💬3   ❤️4
72
🥉
酷豪
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付