canvas 日志工具

342 阅读3分钟

背景

前段时间在做 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})`;
  }
}

npm 包

canvas-log