{T}

Canvas 最佳实践

本文档总结 Canvas 开发的最佳实践,包括代码组织、性能优化、错误处理等方面。

代码组织

使用类封装

javascript
class CanvasApp {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId)
    this.ctx = this.canvas.getContext('2d')
    this.shapes = []
    this.animationId = null
    
    this.init()
  }
  
  init() {
    this.setupCanvas()
    this.bindEvents()
    this.startAnimation()
  }
  
  setupCanvas() {
    // 设置画布尺寸
    this.canvas.width = 800
    this.canvas.height = 600
  }
  
  bindEvents() {
    this.canvas.addEventListener('click', this.handleClick.bind(this))
    window.addEventListener('resize', this.handleResize.bind(this))
  }
  
  handleClick(e) {
    // 处理点击
  }
  
  handleResize() {
    // 处理窗口大小变化
  }
  
  update() {
    // 更新状态
  }
  
  draw() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    // 绘制内容
  }
  
  startAnimation() {
    function animate() {
      this.update()
      this.draw()
      this.animationId = requestAnimationFrame(animate.bind(this))
    }
    animate.call(this)
  }
  
  stopAnimation() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
      this.animationId = null
    }
  }
  
  destroy() {
    this.stopAnimation()
    // 移除事件监听器
    // 清理资源
  }
}

// 使用
const app = new CanvasApp('myCanvas')

模块化设计

javascript
// renderer.js
export class Renderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
  }
  
  clear() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }
  
  drawShape(shape) {
    shape.draw(this.ctx)
  }
}

// shape.js
export class Shape {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  
  draw(ctx) {
    // 子类实现
  }
}

export class Rectangle extends Shape {
  constructor(x, y, width, height, color) {
    super(x, y)
    this.width = width
    this.height = height
    this.color = color
  }
  
  draw(ctx) {
    ctx.fillStyle = this.color
    ctx.fillRect(this.x, this.y, this.width, this.height)
  }
}

// app.js
import { Renderer } from './renderer.js'
import { Rectangle } from './shape.js'

class App {
  constructor(canvas) {
    this.renderer = new Renderer(canvas)
    this.shapes = [
      new Rectangle(10, 10, 100, 80, '#3498db'),
      new Rectangle(130, 10, 100, 80, '#e74c3c')
    ]
  }
  
  render() {
    this.renderer.clear()
    this.shapes.forEach(shape => this.renderer.drawShape(shape))
  }
}

配置对象模式

javascript
const defaultConfig = {
  width: 800,
  height: 600,
  backgroundColor: '#ffffff',
  lineWidth: 2,
  strokeColor: '#333333',
  fillColor: '#3498db'
}

class ConfigurableCanvas {
  constructor(canvas, config = {}) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.config = { ...defaultConfig, ...config }
    
    this.applyConfig()
  }
  
  applyConfig() {
    this.canvas.width = this.config.width
    this.canvas.height = this.config.height
    this.ctx.lineWidth = this.config.lineWidth
    this.ctx.strokeStyle = this.config.strokeColor
    this.ctx.fillStyle = this.config.fillColor
  }
}

Canvas 尺寸设置

使用 HTML 属性设置

html
<!-- ✅ 推荐:使用 HTML 属性 -->
<canvas id="myCanvas" width="800" height="600"></canvas>

<!-- ❌ 不推荐:只使用 CSS -->
<canvas id="myCanvas" style="width: 800px; height: 600px;"></canvas>

响应式尺寸

javascript
class ResponsiveCanvas {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    
    this.handleResize()
    window.addEventListener('resize', () => this.handleResize())
  }
  
  handleResize() {
    const container = this.canvas.parentElement
    const rect = container.getBoundingClientRect()
    const dpr = window.devicePixelRatio || 1
    
    // 设置实际分辨率
    this.canvas.width = rect.width * dpr
    this.canvas.height = rect.height * dpr
    
    // 设置显示尺寸
    this.canvas.style.width = rect.width + 'px'
    this.canvas.style.height = rect.height + 'px'
    
    // 缩放上下文
    this.ctx.scale(dpr, dpr)
  }
}

全屏 Canvas

javascript
function setupFullscreenCanvas(canvas) {
  function resize() {
    canvas.width = window.innerWidth
    canvas.height = window.innerHeight
  }
  
  resize()
  window.addEventListener('resize', resize)
}

错误处理

上下文获取失败

javascript
function getCanvasContext(canvas) {
  if (!canvas) {
    throw new Error('Canvas element not found')
  }
  
  const ctx = canvas.getContext('2d')
  
  if (!ctx) {
    throw new Error('Failed to get 2D context')
  }
  
  return ctx
}

// 使用
try {
  const ctx = getCanvasContext(document.getElementById('myCanvas'))
  // 使用 ctx...
} catch (error) {
  console.error(error.message)
  // 显示后备内容
}

图像加载错误

javascript
function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    
    img.onload = () => resolve(img)
    img.onerror = () => reject(new Error(`Failed to load image: ${src}`))
    
    img.src = src
  })
}

// 使用
async function drawImage() {
  try {
    const img = await loadImage('image.jpg')
    ctx.drawImage(img, 0, 0)
  } catch (error) {
    console.error(error)
    // 绘制占位符
    ctx.fillStyle = '#ccc'
    ctx.fillRect(0, 0, 100, 100)
  }
}

参数验证

javascript
function drawRect(x, y, width, height) {
  if (typeof x !== 'number' || typeof y !== 'number') {
    throw new TypeError('x and y must be numbers')
  }
  
  if (width <= 0 || height <= 0) {
    throw new RangeError('width and height must be positive')
  }
  
  ctx.fillRect(x, y, width, height)
}

// 使用安全调用
function safeDrawRect(...args) {
  try {
    drawRect(...args)
  } catch (error) {
    console.warn(error.message)
  }
}

跨浏览器兼容性

特性检测

javascript
// 检测 Canvas 支持
function isCanvasSupported() {
  const canvas = document.createElement('canvas')
  return !!(canvas.getContext && canvas.getContext('2d'))
}

// 检测 ImageData 支持
function isImageDataSupported() {
  try {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    ctx.createImageData(1, 1)
    return true
  } catch (e) {
    return false
  }
}

// 检测 toBlob 支持
if (!HTMLCanvasElement.prototype.toBlob) {
  HTMLCanvasElement.prototype.toBlob = function(callback, type, quality) {
    const dataURL = this.toDataURL(type, quality)
    // 转换 dataURL 到 Blob
    // ...
  }
}

Polyfills

javascript
// requestAnimationFrame polyfill
if (!window.requestAnimationFrame) {
  window.requestAnimationFrame = 
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function(callback) {
      return setTimeout(callback, 16)
    }
}

// cancelAnimationFrame polyfill
if (!window.cancelAnimationFrame) {
  window.cancelAnimationFrame = 
    window.webkitCancelAnimationFrame ||
    window.mozCancelAnimationFrame ||
    function(id) {
      clearTimeout(id)
    }
}

浏览器特定问题

javascript
// 解决 IE 不支持 addColorStop 的颜色格式问题
function safeAddColorStop(gradient, offset, color) {
  // 确保颜色格式正确
  if (color.startsWith('hsl') || color.startsWith('hsla')) {
    // 转换 HSL 到 RGB
    color = hslToRgb(color)
  }
  gradient.addColorStop(offset, color)
}

// iOS Safari 图像大小限制
function checkImageSize(img) {
  const maxSize = 1024 * 1024 * 4 // 4MP
  const pixels = img.width * img.height
  
  if (pixels > maxSize) {
    console.warn('Image size exceeds iOS Safari limit')
    // 缩小图像
    return scaleDownImage(img, maxSize / pixels)
  }
  
  return img
}

安全性考虑

跨域资源

javascript
// 加载跨域图像
const img = new Image()
img.crossOrigin = 'anonymous' // 需要服务器支持 CORS
img.src = 'https://example.com/image.jpg'

// 服务器需要返回正确的 CORS 头
// Access-Control-Allow-Origin: *

数据 URL 限制

javascript
// 检查 data URL 大小
function toDataURLSafe(canvas, type = 'image/png', quality = 0.92) {
  const dataURL = canvas.toDataURL(type, quality)
  
  // 某些浏览器有 data URL 大小限制
  if (dataURL.length > 2 * 1024 * 1024) {
    console.warn('Data URL exceeds 2MB limit')
    // 使用 Blob 替代
    return new Promise((resolve) => {
      canvas.toBlob(resolve, type, quality)
    })
  }
  
  return Promise.resolve(dataURL)
}

用户输入验证

javascript
// 验证颜色值
function isValidColor(color) {
  // 支持的颜色格式
  const patterns = [
    /^#[0-9A-Fa-f]{6}$/,
    /^#[0-9A-Fa-f]{3}$/,
    /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/,
    /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/,
    /^[a-zA-Z]+$/
  ]
  
  return patterns.some(pattern => pattern.test(color))
}

// 安全设置颜色
function safeSetColor(property, color) {
  if (isValidColor(color)) {
    ctx[property] = color
  } else {
    console.warn(`Invalid color: ${color}`)
    ctx[property] = '#000000'
  }
}

可访问性

提供后备内容

html
<canvas id="chart">
  <p>图表显示销售数据:2023年销售额增长25%</p>
  <table>
    <tr><th>年份</th><th>销售额</th></tr>
    <tr><td>2022</td><td>$100万</td></tr>
    <tr><td>2023</td><td>$125万</td></tr>
  </table>
</canvas>

ARIA 属性

html
<canvas 
  id="game"
  role="img"
  aria-label="太空射击游戏画面"
  aria-describedby="game-description">
</canvas>
<p id="game-description">使用方向键控制飞船,空格键发射子弹</p>

键盘支持

javascript
class AccessibleCanvas {
  constructor(canvas) {
    this.canvas = canvas
    this.canvas.setAttribute('tabindex', '0')
    this.bindKeyboardEvents()
  }
  
  bindKeyboardEvents() {
    this.canvas.addEventListener('keydown', (e) => {
      switch (e.key) {
        case 'ArrowLeft':
          this.moveLeft()
          break
        case 'ArrowRight':
          this.moveRight()
          break
        case 'Enter':
        case ' ':
          this.activate()
          break
      }
    })
  }
  
  moveLeft() {
    // 处理左移
  }
  
  moveRight() {
    // 处理右移
  }
  
  activate() {
    // 处理激活
  }
}

焦点指示

javascript
// Canvas 获得焦点时显示边框
this.canvas.addEventListener('focus', () => {
  this.canvas.style.outline = '2px solid blue'
})

this.canvas.addEventListener('blur', () => {
  this.canvas.style.outline = 'none'
})

测试

单元测试

javascript
describe('Rectangle', () => {
  let ctx
  
  beforeEach(() => {
    const canvas = document.createElement('canvas')
    ctx = canvas.getContext('2d')
  })
  
  it('should draw rectangle correctly', () => {
    const rect = new Rectangle(10, 10, 100, 50, 'red')
    rect.draw(ctx)
    
    // 验证绘制结果
    const imageData = ctx.getImageData(10, 10, 100, 50)
    // 检查像素颜色...
  })
  
  it('should detect point inside', () => {
    const rect = new Rectangle(10, 10, 100, 50, 'red')
    expect(rect.contains(50, 30)).toBe(true)
    expect(rect.contains(5, 5)).toBe(false)
  })
})

性能测试

javascript
function measurePerformance(name, fn, iterations = 100) {
  const start = performance.now()
  
  for (let i = 0; i < iterations; i++) {
    fn()
  }
  
  const end = performance.now()
  const average = (end - start) / iterations
  
  console.log(`${name}: ${average.toFixed(2)}ms per iteration`)
}

measurePerformance('Draw 1000 circles', () => {
  for (let i = 0; i < 1000; i++) {
    ctx.beginPath()
    ctx.arc(Math.random() * 800, Math.random() * 600, 5, 0, Math.PI * 2)
    ctx.fill()
  }
})

总结

遵循这些最佳实践可以:

  1. 提高代码质量:良好的组织和结构
  2. 提升性能:优化渲染和资源使用
  3. 增强稳定性:完善的错误处理
  4. 保证兼容性:跨浏览器支持
  5. 提高可访问性:支持所有用户

下一步学习


返回Canvas 教程目录 | 上一篇API 参考