Добавить динамические значения в консольные методы во время выполнения с сохранением исходной позиции вызова и номера строки без изменений

Я сделал следующий класс, чтобы "захватить" функцию console.log. Причина этого заключается в том, что я хочу добавлять и удалять значения динамически. Он будет использоваться для целей отладки, поэтому важно происхождение вызова функции console.log(). В следующем коде я объясню свою логику в комментариях.

export class ConsoleLog {
  private _isActive = false;
  private _nativeLogFn: any;

  constructor() {

    // ----------------------
    // Store the native console.log function, so it can be restored later 
    // ----------------------

    this._nativeLogFn = console.log;

  }

  public start() {
    if (!this._isActive) {

      // ----------------------
      // Create a new function as replacement for the native console.log 
      // function. *** This will be the subject of my question ***
      // ----------------------

      console.log = console.log.bind(console, Math.random());

      this._isActive = true;
    }
  }

  public stop() {
    if (this._isActive) {
      // Restore to native function
      console.log = this._nativeLogFn;
      this._isActive = false;
    }
  }
}

Проблема с этой установкой заключается в том, что новая функция назначается в статической форме.

// Function random() generates a number at the moment I assign the function. 
// Let say it the number *99* for example sake. 

console.log.bind(console, Math.random());

Каждый раз, когда вызывается console.log(...), он выдает 99. Таким образом, он довольно статичен. (Чтобы быть впереди вас: нет моей цели не выводить случайное число, LOL, но я просто использую его для проверки, является ли выход динамическим или нет.).

Досадная часть, используя функцию с console.log.bind - единственный способ, который я нашел, фактически сохраняя номер вызывающего абонента и номер строки.

Я написал следующий простой тест.

    console.log('Before call, active?', 'no'); // native log

    obj.start();  // Calls start and replaces the console.log function

    console.log('foo'); // This will output 'our' 99 to the console.
    console.log('bar'); // This will output 'our' 99 again.

    obj.stop(); // Here we restore the native console.log function

    console.log('stop called, not active'); // native log again

    // Now if I call it again, the random number has changed. What is 
    // logical, because I re-assign the function.

    obj.start();  // Calls start and replaces the console.log function
    console.log('foo'); // This will output N to the console.
    // But then I have to call start/log/stop all the time. 

Вопрос. Как добавить значения в console.log во время выполнения, не теряя имя и имя вызывающего абонента и номер строки... И без беспокойства потребителя библиотеки, как только этот класс будет запущен с запуском().

EDIT: Добавлен plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0

Ответы

Ответ 1

Потратьте меня на большую часть выходных и много чтения и возиться, но я, наконец, решил использовать его с помощью прокси-объекта ES6. Довольно мощный материал, который я мог бы добавить. Объяснение содержится в коде. Пожалуйста, не стесняйтесь улучшать его или задавать вопросы.

(EDITED на основе комментариев @Bergi) Вот класс:

export class ConsoleLog {
  private _isActive = false;
  private _nativeConsole: any;
  private _proxiedConsole: any;

  /**
   * The Proxy constructor takes two arguments, an initial Object that you
   * want to wrap with the proxy and a set of handler hooks.
   * In other words, Proxies return a new (proxy) object which wraps the
   * passed in object, but anything you do with either effects the other.
   *
   * ref: https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies
   * ref: http://exploringjs.com/es6/ch_proxies.html#_intercepting-method-calls
   */

  /**
   * Challenge:
   * When we intercept a method call via a proxy, you can intercept the
   * operation 'get' (getting property values) and you can intercept the
   * operation 'apply' (calling a function), but there is no single operation
   * for method calls that you could intercept. That’s why we need to treat
   * them as two separate operations:
   *
   * First 'get' to retrieve a function, then an 'apply' to call that
   * function. Therefore intercepting 'get' and return a function that
   * executes the function 'call'.
   */

  private _createProxy(originalObj: Object) {

    const handler = {

      /**
       * 'get' is the trap-function.
       * It will be invoked instead of the original method.
       * e.a. console.log() will call: get(console, log) {}
       */
      get(target: object, property: string) {

        /**
         * In this case, we use the trap as an interceptor. Meaning:
         * We use this proxy as a sort of pre-function call.
         * Important: This won't get invoked until a call to a the actual
         * method is made.
         */

        /**
         * We grab the native method.
         * This is the native method/function of your original/target object.
         * e.a. console.log = console['log'] = target[property]
         * e.a. console.info = console['info'] = target[property]
         */
        const nativeFn: Function = target[property];

        /**
         * Here we bind the native method and add our dynamic content
         */
        return nativeFn.bind(
          this, `%cI have dynamic content: ${Math.random()}`, 'color:' +
          ' #f00;'
        );
      }
    };
    return new Proxy(originalObj, handler);
  }

  constructor() {
    // Store the native console.log function so we can put it back later
    this._nativeConsole = console;
    // Create a proxy for the console Object
    this._proxiedConsole = this._createProxy(console);
  }

  // ----------------------
  // (Public) methods
  // ----------------------

  public start() {
    if (!this._isActive) {
      /**
       * Replace the native console object with our proxied console object.
       */
      console = <Console>this._proxiedConsole;
      this._isActive = true;
    }
  }

  public stop() {
    if (this._isActive) {
      // Restore to native console object
      console = <Console>this._nativeConsole;
      this._isActive = false;
    }
  }
}

И вот код, чтобы убедиться в этом:

const c: ConsoleLog = new ConsoleLog();

console.log('Hi, I am a normal console.log', ['hello', 'world']);

c.start(); // Start - replaces the console with the proxy

console.log('Hi, I am a proxied console.log');
console.log('I have dynamic content added!');
console.log('My source file and line number are also intact');

c.stop(); // Stop - replaces the proxy back to the original.

console.log('I am a normal again');

Ура!

Ответ 2

Как насчет:

const consolelog = console.log;
console.log = function (...args) {
    return consolelog.apply(this, [Math.random()].concat(args));
}

Обратите внимание, что this внутри функции не экземпляр вашего класса.
Функция представляет собой обычную анонимную функцию, а не функцию стрелки, так что область видимости функции будет зависеть от выполнения.


Изменить

Хорошо, без apply, это еще лучше:

console.log = function (...args) {
    return consolelog(Math.random(), ...args);
}

2nd edit

Я собирался сказать, что это невозможно, но потом у меня был прорыв:

function fn() {
    return Math.random();
}
fn.valueOf = function () {
    return this();
};
console.log = consolelog.bind(console, fn);

Затем это: console.log("message") выведет что-то вроде:

function 0.4907970049205219 "message"

С правильным абонентом, но я не мог удалить часть function в начале.
Затем у меня был еще один прорыв:

function fn() {
    return Math.random();
}
fn.toString = function () {
    return this().toString();
}
console.log = consolelog.bind(console, "%s", fn);

Затем это: console.log("message") выводит:

0,9186478227998554 сообщение

С правильным абонентом, как вы просили.

Он работает только при привязке его к функции, использование других объектов не работает.

Ответ 3

Если вы хотите динамически связывать свою функцию, вы можете сделать это при каждом доступе к свойству .log. Для этого достаточно простого геттера, нет необходимости использовать прокси ES6:

export class ConsoleLog {
  constructor(message) {
    this._isActive = false;
    const nativeLog = console.log;
    Object.defineProperty(console, "log", {
      get: () => {
        if (this._isActive)
          return nativeLog.bind(console, message())
        return nativeLog;
      },
      configurable: true
    });
  }
  start() {
    this._isActive = true;
  }
  stop() {
    this._isActive = false;
  }
}

new ConsoleLog(Math.random).start();

Ответ 4

Этот ответ показывает, как использовать прокси и Object.bind для ввода аргументов в существующие (объект/API) функции.

Это работает с консолью, сохраняя номер строки консолей и ссылку на файл.

// targetName is the name of the window object you want to inject arguments
// returns an injector object.
function injector(targetName){
    const injectors = {};  // holds named injector functions
    const _target = window[targetName];  // shadow of target
    const proxy = new Proxy(_target, {
        get: function(target, name) {
            if (typeof injectors[name] === "function" &&
                typeof _target[name] === "function") {  // if both _target and injector a function 
                return _target[name].bind(_target, ...injectors[name]()); 
            }
            return _target[name];
        },
    });
    return {
        enable () { window[targetName] = proxy; return this },
        disable () { window[targetName] = _target },
        injector (name, func) { injectors[name] = func },
    };
};

Чтобы использовать

// Example argument injector.
// Injector functions returns an array of arguments to inject
const logInfo = {
    count : 0,
    counter () { return ["ID : " + (logInfo.count++) + ":"] },
    mode(){ return ["App closing"] },
}

Создание пускового инжектора

// Create an injector for console
const consoleInjector = injector("console");

Функции инжектора enable, injector, disable использование

// Enable consoleInjector and add injector function.
consoleInjector.enable().injector("log", logInfo.counter);
console.log("testA"); // >> ID : 0: testA VM4115:29 
console.log("testB"); // >> ID : 1: testB VM4115:31 

// Replace existing injector function with another one.
consoleInjector.injector("log",logInfo.mode);  // change the injector function
console.log("testC");  // >> App closing testC VM4115:34 
console.log("testD",1,2,3,4); // App closing testD 1 2 3 4 VM4115:35 

// Turn off console.log injector
consoleInjector.injector("log",undefined);   

// or/and turns off injector and return console  to normal
consoleInjector.disable();
console.log("testE"); // testE  VM4115:42