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()
}
})总结
遵循这些最佳实践可以:
- 提高代码质量:良好的组织和结构
- 提升性能:优化渲染和资源使用
- 增强稳定性:完善的错误处理
- 保证兼容性:跨浏览器支持
- 提高可访问性:支持所有用户
下一步学习
返回:Canvas 教程目录 | 上一篇:API 参考