背景
前段时间在做 canvas 相关的工作, 业务中调用大量的 canvas 渲染方法. 这些方法的调用, 分散在不同的模块中. 有时候某处渲染出错, 一时很难定位到是哪个地方调用的不对. 整个流程调用了哪些方法也没有记录. 于是稍微写了一个小工具, 用来记录 canvas 的各种方法调用. 算是个调试工具.
用法
canvas 方法调用日志
import CanvasLog from 'canvas-log';
const ctx = document.querySelector('#c').getContext('2d');
const logger = new CanvasLog(ctx);
// 普通的 canvas 渲染逻辑
ctx.scale(1, 1)
ctx.save()
ctx.strokeStyle = 'red'
ctx.lineWidth = 5
ctx.translate(100, 100)
ctx.moveTo(0, 0)
ctx.lineTo(0, 60)
ctx.lineTo(90, 60)
ctx.lineTo(90, 0)
ctx.closePath()
ctx.stroke()
// 获取调用记录(对象数组)
console.info(logger.getAllRecords())
// [
// {method: 'scale', args: [1, 1], invokeAt: 1640746288817},
// {method: 'save', args: [], invokeAt: 1640746288818},
// {method: 'translate', args: [100, 100], invokeAt: 1640746288818},
// {method: 'moveTo', args: [0, 0], invokeAt: 1640746288818},
// {method: 'lineTo', args: [0, 60], invokeAt: 1640746288818},
// {method: 'lineTo', args: [90, 60], invokeAt: 1640746288818},
// {method: 'lineTo', args: [90, 0], invokeAt: 1640746288818},
// {method: 'closePath', args: [], invokeAt: 1640746288818},
// {method: 'stroke', args: [], invokeAt: 1640746288818},
// ]
// 获取字符串模式的调用日志
console.info(logger.getAllLogs())
// [
// "scale(1, 1)",
// "save()",
// "translate(100, 100)",
// "moveTo(0, 0)",
// "lineTo(0, 60)",
// "lineTo(90, 60)",
// "lineTo(90, 0)",
// "closePath()",
// "stroke()",
// ]
// 根据条件, 搜索某种调用记录
console.info(logger.filterRecords(item => item.method === 'lineTo'))
// [
// {method: 'lineTo', args: [0, 60], invokeAt: 1640746288818},
// {method: 'lineTo', args: [90, 60], invokeAt: 1640746288818},
// {method: 'lineTo', args: [90, 0], invokeAt: 1640746288818},
// ]
canvas 方法添加钩子
import CanvasLog from 'canvas-log';
const ctx = document.querySelector('#c').getContext('2d');
const logger = new CanvasLog(ctx, {
hooks: {
all: { // 全局钩子, 每个 canvas 方法调用都执行
before(method, params) {
console.info(`[all] before ${method} call`, params);
},
after(method, params) {
console.info(`[all] after ${method} call`, params);
},
},
moveTo: { // ctx.moveTo() 前后执行
before(method, params) {
console.info(`<moveTo>`, params);
},
after(method, params) {
console.info(`</moveTo>`, params);
},
},
closePath: { // ctx.closePath() 前后执行
before(method, params) {
console.info(`before closePath`, params);
},
after(method, params) {
console.info(`after closePath`, params);
},
},
},
});
// 普通的 canvas 渲染逻辑
ctx.moveTo(0, 0)
ctx.lineTo(0, 60)
ctx.lineTo(90, 60)
ctx.closePath()
ctx.stroke()
// 钩子的执行结果
// [all] before moveTo call [0, 0]
// <moveTo> [0, 0]
// </moveTo> [0, 0]
// [all] after moveTo call [0, 0]
// [all] before lineTo call [0, 60]
// [all] after lineTo call [0, 60]
// [all] before lineTo call [90, 60]
// [all] after lineTo call [90, 60]
// [all] before closePath call []
// before closePath []
// after closePath []
// [all] after closePath call []
// [all] before stroke call []
// [all] after stroke call []
思路
很简单, 对 canvas 原型上的方法进行二次封装, 在原方法调用时添加一条调用记录. 相当于增加了一个用于记录日志的切面.
其他功能
既然能加日志切面, 也可以加其他切面. 于是添加了一个钩子功能. 可以在canvas调用特定方法之前/之后执行钩子, 也可以在每个方法之前/之后执行一个通用的钩子.
源码
index.ts
import CanvasRecord from './canvas-record';
type Hook = (method?: string, args?: any[]) => any;
type Hooks = {
[prop: string]: {
before: Hook;
after: Hook;
};
};
type Options = {
withParams: boolean;
hooks?: Hooks;
};
const defaultOptions: Options = {
withParams: true,
};
const noop = () => {};
const toFunction = (fn: any) => (typeof fn === 'function') ? fn : noop;
export default class CanvasLogger {
records: CanvasRecord[] = [];
constructor(canvasCtx: any, options: Options = defaultOptions) {
const canvasProto = Object.getPrototypeOf(canvasCtx);
const records = this.records;
const protoFnNames = Object
.keys(canvasProto)
.filter((key) => typeof canvasCtx[key] === 'function');
protoFnNames.forEach((method) => {
const originFn = canvasCtx[method];
canvasCtx[method] = function (...args: any) {
records.push(new CanvasRecord(method, options.withParams ? args : undefined));
toFunction(options.hooks?.all?.before)(method, args);
toFunction(options.hooks?.[method]?.before)(method, args);
const ret = originFn.call(canvasCtx, ...args);
toFunction(options.hooks?.[method]?.after)(method, args);
toFunction(options.hooks?.all?.after)(method, args);
return ret;
};
});
}
getAllRecords() {
return this.records;
}
getAllLogs(formatter?: (r: any) => string) {
return this.records.map((record) => {
return (typeof formatter === 'function') ? formatter(record) : record.toString();
});
}
filterRecords(filter: (item: CanvasRecord) => boolean) {
return this.records.filter(filter);
}
}
canvas-record.ts
export default class CanvasRecord {
method: string;
args: any[];
invokeAt: number;
constructor(method: string, args: any) {
this.method = method;
this.args = args;
this.invokeAt = Date.now();
}
toString() {
const argsText = (this.args || []).map((arg: any) => {
if (Array.isArray(arg)) return `[${arg.toString()}]`;
if (typeof arg === 'string') return `'${arg}'`;
return arg;
}).join(', ');
return `${this.method}(${argsText})`;
}
}