javascript typescript ecmascript-6 console.log es6-proxy

javascript - Agregue valores dinámicos a los métodos de la consola en tiempo de ejecución con la conservación de la posición original de la llamada y el número de línea intactos



typescript ecmascript-6 (4)

Hice la siguiente clase para ''secuestrar'' la función console.log . La razón detrás de esto es que quiero agregar y eliminar valores dinámicamente . Se utilizará para fines de depuración, por lo que el origen de la función llamada 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(...) , generará 99 . Así que es bastante estático. (Para estar delante de ti: no, mi objetivo no es generar un número aleatorio, lol, pero lo uso 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 forma en que encontré que realmente conserva el origen de la persona que llama y el número de línea.

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 el tiempo de ejecución sin perder el nombre de archivo y el número de línea del llamante de origen ... Y sin molestar al consumidor de la biblioteca una vez que esta clase se inicia con start ()?

EDITAR: Agregado un plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0


Esta respuesta muestra cómo usar un proxy y Object.bind para inyectar argumentos en las funciones existentes (objeto / API).

Esto funciona con la consola que conserva el número de línea de las consolas y la referencia del archivo.

// 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 }, }; };

Usar

// 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"] }, }

Instalar un inyector de consola

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

Funciones del inyector enable , injector , disable uso

// 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


Me costó la mayor parte del fin de semana y me costó mucho leer y jugar, pero finalmente lo resolví aprovechando el objeto proxy ES6. Cosas bastante poderosas que podría agregar. La explicación está en el código. Por favor no dude en mejorarlo o haga preguntas.

(EDITADO basado en los comentarios de @Gergi) 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 ver por ti 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'');

¡Aclamaciones!


Qué tal si:

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

Tenga en cuenta que this dentro de la función no es la instancia de su clase.
La función es una función anónima normal y no una función de flecha, por lo que el alcance de la función dependerá de la ejecución.

Editar

Ok, sin apply , esto es aún mejor:

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

2ª edición

Estaba a punto de decir que no es posible, pero luego tuve un gran avance:

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

Entonces esto: console.log("message") generará algo como:

función 0.4907970049205219 "mensaje"

Con el interlocutor correcto, pero no pude eliminar la parte de la function al principio.
Entonces tuve otro gran avance:

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

Entonces esto: console.log("message") dará salida:

0.9186478227998554 mensaje

Con la persona que llama correcta, como usted solicitó.

Solo funciona cuando lo vincula a una función, el uso de otros objetos no funciona.


Si desea vincular dinámicamente su función, puede hacerlo en cada acceso a la propiedad .log . Un simple captador es suficiente para eso, no hay necesidad de emplear proxies 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();