javascript - Agregar valores dinámicos a los métodos de la consola en tiempo de ejecución con preservación de la posición de llamada original y el número de línea intactos
He creado la siguiente clase para 'secuestrar' la función console.log. La razón de esto es que quiero agregar y eliminar valores dinámicamente. Se utilizará con fines de depuración, por lo que el origen de la llamada a la función console.log() es importante. En el siguiente código explicaré mi lógica en los comentarios.
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;
}
}
}
El problema con esta configuración es que la nueva función se asigna de forma estática.
// Function random() generates a number at the moment I assign the function.
// Let's say it's the number *99* for example sake.
console.log.bind(console, Math.random());
Cada vez que se llama a console.log(...), se mostrará 99. Así que es bastante estático. (Para adelantarte: no, mi objetivo no es mostrar un número aleatorio, jaja, pero lo uso solo para probar si la salida es dinámica o no).
La parte molesta es que usar la función con console.log.bind es la única manera que encontré que realmente preserva el llamador y el número de línea de origen.
Escribí la siguiente prueba simple.
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.
Pregunta: ¿Cómo puedo agregar valores al console.log en tiempo de ejecución sin perder el nombre de archivo y el número de línea del llamador original... Y sin molestar al consumidor de la biblioteca una vez que esta clase se inicie con start()?
EDITAR: Agregado un plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0
Respuesta #1
Me costó gran parte del fin de semana y mucho leer y trastear, pero finalmente lo resolví aprovechando el objeto proxy de ES6. Bastante potente, debo añadir. La explicación está en el código. Por favor, no dudes en mejorarlo o hacer preguntas.
(EDITADO según los comentarios de @Bergi) Aquí está la clase:
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;
}
}
}
Y aquí el código para que lo veas tú mismo:
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');
¡Salud!
Respuesta #2
Si estás buscando enlazar tu función de manera dinámica, podrías hacerlo en cada acceso a la propiedad .log. Un simple getter es suficiente para eso, no es necesario emplear proxies de 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();
