Práctica de uso compartido y reutilización de depuración de puntos de interrupción para grandes proyectos de front-end

Autor: enoyao, ingenieros de Tencent

antecedentes

A medida que nuestro proyecto se hace más y más grande, es posible que necesitemos mantener muchos módulos. Nuestro proyecto Tencent Document Excel tiene más de 10 módulos grandes, y cada módulo grande tiene N módulos pequeños y cada módulo grande tiene módulos pequeños. La persona principal a cargo está dando seguimiento a los problemas del módulo.

Esto dará lugar a un gran problema, en la mayoría de los casos, el responsable del módulo solo presta atención a los problemas de su propio módulo y no conoce mucho sobre los problemas específicos de los módulos de otras personas a cargo.

Por ejemplo: cuando tenemos comentarios de los usuarios de que hay un problema con copiar y pegar, y queremos localizar rápidamente el problema, solo podemos encontrar a la persona a cargo del módulo de copiar y pegar para que se ocupe de ello. Si la persona a cargo del módulo de copiar y pegar pide salir, entonces otras personas a cargo Al lidiar con este problema, el costo de la solución será muy alto, porque otras personas responsables pueden no estar familiarizadas con este módulo en absoluto.

Otro ejemplo: cuando tenemos algunos compañeros nuevos y queremos que él solucione rápidamente los comentarios de los usuarios, solo podemos enseñarle nuestra experiencia en la depuración de este módulo y decirle los hoyos familiares u organizarlos Muéstrele el iwiki correspondiente (¡generalmente de baja eficiencia y nadie lo ve!), Déjelo ubicar lentamente el problema, para que cada nuevo estudiante esté familiarizado con el módulo, el costo de aprendizaje y mantenimiento se volverá cada vez más caro ¡Cuanto mayor sea el proyecto, más grave será esta situación!

Así que hemos pensado mucho en cómo solucionar estos problemas, al menos para reducir el costo de mantenimiento del módulo y mejorar el mantenimiento y la localización del problema.

Programa

Debido a que el problema anterior es realmente doloroso, hemos explorado gradualmente un conjunto de soluciones en el rastreo y el rodaje. Llamémoslo una solución práctica compartida y reutilizada basada en la depuración de puntos de interrupción. Aquí hay una palabra clave: punto de interrupción En comparación con la familiaridad de todos los desarrolladores, cuando tenemos problemas de posicionamiento de módulos y front-end, inevitablemente usamos puntos de interrupción para romper algunas áreas clave de la operación del código. Aquí hay un ejemplo:

class CopyPaste {
    // 内部粘贴
    pasteFromInter(){ ...}
    // 外部粘贴
    pasteFromOuter(){ debugger; ...}
    // 外部图文粘贴
    isShapePasteFromOuter(){ ... }
    // 外部图片粘贴
    isImgPasteFromOuter(){ ... }
    // 外部文本粘贴
    isTextFromOuter(){ ... }
}

El código anterior es cuando el usuario retroalimenta un problema de copiar y pegar, la persona a cargo que está familiarizada con el módulo sabe que hay un problema con el pegado externo de acuerdo con los comentarios del usuario, debido a que está familiarizado con el módulo, lo mostrará rápidamente en el navegador. punto de interrupción de la consola en el código fuente o inyectar debuggerpalabras clave manualmente para ubicar el problema del usuario paso a paso, primero verificará el pegado interno pasteFromOutersi se activa, luego verificará que la función se isShapePasteFromOuteresté ejecutando correctamente, los parámetros y los parámetros sean correctos, si el código a seguir Torcido, vete isImgPasteFromOuter.

Luego, después de solucionar el problema y reparar el problema, dé un largo suspiro de alivio. Cuando encuentre el siguiente problema, limpie los rastros de depuración actuales en el navegador o el código, y repita la serie de acciones anterior una y otra vez. Creo La mayoría de los estudiantes repiten las acciones similares anteriores todos los días cuando resuelven problemas o incluso hacen necesidades. ¿Podemos considerar guardar estos valiosos rastros de depuración, y cuando nosotros u otros estudiantes encontremos problemas de módulos similares, tomaremos estos ¿El viaje mental que condensa nuestra sangre y lágrimas se repetirá automáticamente?

segmento de código Registra la ubicación del depurador
pasteFromInter 2 filas y 4 columnas
isShapePasteFromOuter 256 filas y 89 columnas
isImgPasteFromOuter 867 filas y 12 columnas

Para proyectos grandes, el costo de tiempo de cada enlace de depuración de errores pequeños es extremadamente grande y también es difícil de reproducir y reproducir Lo que podemos hacer es reutilizar otros similares cuando nos encontremos con problemas similares nuevamente. Experiencia de depuración. Ha habido rastros y experiencias de lesiones, cuando los problemas se vuelvan a encontrar, deberíamos estar más seguros y tranquilos.

Entonces, nuestra primera tarea se ha convertido en retener los enlaces de depuración preciosos, es decir, retener innumerables días y noches, cada punto de ruptura que atraviesa y pica profundamente en nuestros corazones.

Enchufar

En el curso de la práctica, hemos probado innumerables métodos. La primera solución es implementar la retención de puntos de interrupción basada en un complemento del navegador. Basado en la interfaz proporcionada por el desarrollo del complemento de Google Chrome chrome.debugger, es un método de transmisión de mensajes del protocolo de depuración remota de Chrome. chrome.debuggerPuede adjuntar a una o más pestañas para depurar JavaScript. Y utilice el depurador para realizar comunicaciones de complemento basadas en sendCommand y onEvent. Nos permite depurar la página en el complemento. Muchos complementos y herramientas se comunican con la consola del navegador basándose en este protocolo. Esta solución solo puede implementar un panel de depuración remota, que es similar a la depuración del propio navegador. La interfaz puede cargar código y registrar puntos de interrupción y, finalmente, compartir estos puntos de interrupción.

Este tipo de experiencia con la solución será peor. En primer lugar, el panel de depuración implementado por el complemento en sí no puede ser tan bueno como Google Chrome. En segundo lugar, el complemento debe desarrollarse e instalarse de forma activa. La premisa de compartir es que ambas partes deben instalar el complemento correspondiente, desarrollar y promover Los costos son altos, por lo que personalmente no lo recomiendo, pero esto no significa que este programa no lleve a ninguna parte, porque el complemento también puede basarse en otra realización, es el siguiente debugprograma de funciones.

función de depuración

DETALLADO usando el punto de interrupción de la función debug(functionName) y los undebug(functionName)métodos, donde la función de depuración es functionName. Podemos ser debug()insertados en el código (este método y la declaración console.log () es similar), también se puede llamar desde la consola DevTools. debug()Es equivalente a establecer un punto de interrupción de línea de código en la primera línea de función.

Generalmente se usa en la consola, este método con el complemento tendrá una mejor experiencia, ya que el complemento utiliza chrome.devtools.inspectedWindow.evalun método con una interfaz de navegador que puede inyectar código en la consola para realizarlo para lograr ayudarlo a emitir automáticamente la función de punto de interrupción.

chrome.devtools.inspectedWindow.eval(
  `debug(window.xxxApi);`,
  (value) => {
    callback && callback(value);
  }
);

Pero los estudiantes cuidadosos encuentran que yo uso el debugmonitor de funciones es una función global window.xxxApi, así que aquí resumimos la experiencia, el inconveniente de este enfoque es que si usa la consola, buscará la función en su contexto, por lo que Por lo general, solo se puede usar para la administración de funciones globales. Si la función que se va a colocar con puntos no está en el contexto, debe establecer un punto de interrupción manual en el alcance de la función de destino y luego usar la función para activarla. Si es una función de cierre, no hay forma, pero los defectos no están ocultos. , Este método puede ayudarnos a localizar rápidamente cualquier función global, incluso si el código está confuso, aún se puede leer rápidamente y agregarle puntos de interrupción de función, por lo que sugiero esta solución como alternativa, en algunos casos ¡Puede jugar un efecto milagroso!

Inyección de AST

Después de experimentar los diversos pozos anteriores, introduzcamos brevemente un conjunto de soluciones que hemos implementado:

Nuestro programa está de hecho antes que las soluciones de cadena de llamadas de función realizadas sobre la base de una mejora, ya que podemos desarrollar su propia debuggerpalabra clave de entrada de código para que el código viva en cualquier lugar, ¿por qué no ponemos esta herramienta a trabajar?

En primer lugar, podemos usar la máquina de estado para decirle a la herramienta dónde necesitamos distribuir la ubicación de RBI, similar a nuestra tabla de configuración de silbidos de uso común:

Module 'CopyPaste'
    index.ts -f pasteFromInter -s !(()=>{ console.log(window.Worker) })()
    index.ts -f pasteFromOuter -s console.log('success') -check messagecenter1
    index.ts -f isShapePasteFromOuter
End Module
  • Module <-- state --> End Module Aquí se describe un estado, que es un comportamiento de distribución de puntos de interrupción, que se utiliza para monitorear ese tipo de módulo, por ejemplo: módulo de copiar y pegar, módulo de capa de datos o módulo de capa de datos

  • -f functionname -s codeEste estado puede describirse en este documento características de comportamiento específicas tales como: los pasteFromInterpuntos de interrupción de la función de distribución y el debuggercódigo inyectado .

En el paquete web, podemos analizar este archivo de configuración en los dos procesos de cargador o complemento. Aquí también puede utilizar bibliotecas de terceros o usuarios habituales para analizar el texto de estado anterior. Fui el cargador para resolver esta tabla de estado I en el directorio global o localmente dentro de una definición de módulo .debug.jsonpara escribir el estado descrito anteriormente, luego se analiza un objeto de mapa:

args = argument({
    "--class": String, // 类
    "--function": String, // 函数
    "--code": String, // 函数
    "-c": "--class", // 转义替换
    "-f": "--function",
    "-s": "--code",
  },{ argv: debugConfigValue, }
);

Si no desea escribir la forma en que el archivo de configuración de la máquina de estado, de hecho, puede usar un debug.jsonarchivo para describir la ubicación de un punto de interrupción, este enfoque es más simple, el archivo json de análisis de costos es menor que muchos de los archivos de configuración de la máquina de estado, archivo json Los campos principales involucrados aquí son la ruta del código que necesita ser detectado, esta conveniente herramienta para ubicar el archivo y luego el nombre de la clase o función que necesita ser detectada, esta conveniente herramienta para ubicar la ubicación del código y el nombre del elemento de detección y la necesidad de ser detectado. El código y un valor de clave clave:

{
  "MessageCenter": {
    "function": [
      {
        "path": "src/core/network/message-center/SendMessageCenter.ts",
        "name": "_sendUserChanges",
        "title": "数据层断点测试2",
        "code": "__console.log('数据层断点测试2')",
        "key": "MessageCenter|function|1"
      }
    ]
  }
}

La clave puede definirse en este documento se relaciona con un punto claro, tal MessageCenter|function|1medio es una función de un RBI dentro del módulo de archivo de MessageCenter, para continuar mejorando en el futuro también se puede escribir MessageCenter|class|1:12, significa que una clase particular de una cierta posición de punto en el archivo de MessageCenter dentro del módulo, Si la semántica de esta clave es más rica, la distribución posterior será más precisa y el problema de posicionamiento será más eficiente.Los detalles se pueden definir según el escenario empresarial.

class CopyPaste {
    // 内部粘贴
    pasteFromInter(){
        debugger
        ...
    }
}

Cuando tenemos el archivo de configuración, tenemos que pensar en cómo agregar código de depuración y detección al código sin intrusiones. Preferimos inyectar a través de AST, que puede ayudarnos a clasificar las partes clave del código en un árbol, como borrar Dos puntos, paréntesis, punto y coma, etc., nos permiten enfocarnos en los nodos importantes.Después de analizar el código anterior, se obtendrá el siguiente árbol de sintaxis AST:

{
  "program": {
    "type": "Program",
    "body": [{
      "type": "ClassDeclaration",
      "id": {
    
    { "type": "Identifier", "identifierName": "CopyPaste" }, "name": "CopyPaste" },
      "body": {
        "type": "ClassBody",
        "body": [{
            "type": "ClassMethod",
            "key": { "type": "Identifier", "name": "pasteFromInter" },
            "body": { "type": "BlockStatement", "body": [{ "type": "DebuggerStatement" }]},
            "leadingComments": [{ "type": "CommentLine", "value": " 内部粘贴" }],
        }]
      }
    }]
  }
}

Probablemente los pasos específicos son los siguientes: analizar MessageCenter|function|1una cadena de estos parámetros de configuración para obtener el nombre de la función, el nombre del módulo, la información de ubicación, etc., y luego escanea el código y el análisis sintáctico y léxico, el árbol sintáctico AST y obtenido de acuerdo con la función recién analizada Nombre, nombre del módulo, información de ubicación para que coincida con el nodo del árbol AST, agregue nuestro código de depuración y detección en él, y finalmente genere el código que ha sido procesado por nosotros.

Todos conocemos los principios anteriores. Podemos usar complementos para implementarlo en la herramienta de paquete web. En los complementos, a menudo usamos el modo de visitante, lo que significa que cuando accedemos a una ruta determinada, la hacemos coincidir y luego Esta modificación del nodo, como la pasteFromInterfunción anterior , es una ClassMethod, el código generado hará que los complementos visiten el árbol AST, el visitante puede coincidir con cualquier característica léxica correspondiente, aquí podemos unir todos y ClassMethodluego obtener la ruta correspondiente a la información del nodo, como nombres de función, argumentos de función, y una función de ubicación, etc., para obtener la información clave, podemos realizar la función del nodo de procesamiento, al cual se le inyecta nuestro código de puesta en marcha y prueba o una inyección directa debuggerpara interrumpir punto.

plugins = {
  // 访问器
  Visitor = {
      'ClassMethod'(path) {
        // 检点
        path.node
      }
  }
}

Por supuesto, se requiere el código de detección de inyección para construir ClassMethoduna estructura similar, todo lo que podemos con las @babel/typesherramientas para inyectar rápidamente un fragmento de código, como la más simple es inyectar un debugger:

types.expressionStatement(types.identifier(`debugger`))

Esto colocará uno en una ubicación específica de su ruta coincidente debugger, y su archivo fuente de código en sí no se ha cambiado de ninguna manera, pero un fragmento de código se fusionó con éxito en la ubicación especificada a través del árbol AST y el archivo de configuración. Por supuesto, la situación real será Es más complicado de lo esperado, porque es posible que la ubicación de la entrega no sea una determinada posición en la función, puede ser una determinada posición en la función de clase, una determinada posición en la función de cierre, por lo que debe ser compatible con varias estructuras gramaticales. Solo al hacer coincidir todas las características de estas funciones en el AST se puede entregar el código con precisión, o tomar la función como ejemplo y enumerar algunas de las situaciones que deben considerarse:

  • FunctionExpression

Estos dos métodos de escritura deben satisfacerse, de lo contrario, el depurador enviará la posición incorrecta.

this.xxx = function() { debugger }
const xxx = function() { debugger }
  • ClassMethod

Esta situación general se puede ubicar de la siguiente manera, pero si desea ser más preciso, como una función privada, debe escribir un descriptor de acceso más preciso.

class xxx { xxx:(){ debugger } }
  • FunciónDeclaración

Además de tratar con la escritura de la expresión de la función anterior, no olvide que la función también tiene la escritura de la definición de declaración, por lo que debe estar completa.

function xxx() { debugger }
  • ArrowFunctionExpression

Finalmente, considere la escritura de la función de flecha hacia abajo

const xxx = () => { debugger }
this.xxx = () => { debugger }
class xxx { xxx = () => { debugger } }

Aunque en la mayoría de los casos la función de coincidencia puede cubrir la mayoría de las escenas del código de depuración emitido por el proyecto, siempre habrá un pez en la red. Por ejemplo, si algunos estudiantes quieren inyectar el código de detección antes de la definición de la clase, deben continuar escribiendo el descriptor de acceso correspondiente. Para obtener la ruta y luego distribuir el código de detección correspondiente a la ubicación, debe estar familiarizado con varias sintaxis y los tipos de acceso correspondientes para lograrlo sin problemas.

Después de la transformación anterior, obtendremos un nuevo código en el código final (se ha inyectado todo el código de detección), pero esto activará uno nuevo.Cuando ejecutemos este nuevo código, todos los códigos de detección anteriores se ejecutarán nuevamente. Esto romperá muchas áreas de código que otros líderes de módulos no quieren romper, por lo que en realidad necesitamos distribuir un código de detección con un interruptor. Por supuesto, la participación de este interruptor puede ser muy simple, de la siguiente manera:

// 基于 AST 在模块中分发的调试开关
if(require('@tencent/vdebugger').call(this, key)){ debugger }
// 或者这样,虽然好看点,但这样 debugger 在闭包里面拿不到上下文
require('@tencent/vdebugger').call(this, key) || (() => { debugger })()
// 注意这种下面类似这种写法是不行的↓
require('@tencent/vdebugger') || debugger

Podemos usar el require('@tencent/vdebugger')paquete una función que puede diseñarse para leer la configuración en una variable global o localstorage y otros lugares, y luego devuelve un valor booleano que determina si la posición debuggerdonde para poder depurar la conveniencia hay varios pequeños detalles que necesitan atención, debuggerEstas palabras clave tienen que separar un alcance, por lo que no puede escribir algo similar a esto false || debugger, y require('@tencent/vdebugger')esta función después de leer la configuración dentro del paquete, puede ser un evalmétodo para realizar el código de detección, por lo que puede usar callla agencia sobre el alcance actual, más conveniente Ve a depurar.

Por supuesto, la situación real puede ser más complicada de lo imaginado. Tomemos un ejemplo simple: debido a que el conmutador distribuido puede inyectarse en algún código empaquetado en el trabajador, el trabajador se usa mucho en proyectos grandes, pero el trabajador no puede leerlo. Documento, ventana, estos objetos, aunque se pueden usar el navegador, la ubicación y XMLHttpRequest, pero no se pueden controlar mediante la configuración de lectura del almacenamiento local y otros medios, por lo que debe considerar si necesita distribuir el conmutador de depuración al código de trabajo. Cómo comunicar el interruptor correspondiente y otras cuestiones.

Lo más simple y grosero es filtrar al empaquetar el código de trabajador.

!isWorker && new DebuggerPlugin({
    debugConfig: path.resolve(dirName, '../debug.json'),
}),

Por supuesto, si necesita distribuir el trabajador de efecto de conmutación, debe implementar un medio de comunicación conmutador configurado para leer, el medio de comunicación más común se basa en postMessage, de modo que require('@tencent/vdebugger')la función que el módulo conmutador acepte la configuración del hilo principal ejecutando código de localización trabajador Emita el comando si ejecutar el código de detección e iniciar el punto de interrupción.

myWorker.postMessage(xx);
myWorker.onmessage = () => {
  console.log('Message received from worker');
}

Pensando

Después de implementar las funciones básicas anteriores, podemos continuar optimizando muchas experiencias. Por ejemplo, también podemos usar el complemento webpack para implementar actualizaciones incrementales durante la compilación local. Esto se puede lograr cuando cambiamos el archivo de configuración local, distribuir automáticamente puntos de interrupción y código de depuración, la lógica es relativamente simple, utilizando la biblioteca de complementos de ciclo de aplicación incorporada chokidarpara monitorear el cambio del archivo de configuración, y luego compilar el disparador, volver a tomar el AST para depurar el código compilado junto con el código de punto de interrupción:

const chokidar = require('chokidar');
this.watcher = chokidar.watch(["../src/**/.debug.json"], {
  usePolling: true,
  ignored: this.options.ignored
});

para resumir

No hay muchos artículos relacionados con la depuración sobre este aspecto, y se han saltado muchos pozos en el camino. Gracias por el apoyo de los miembros del equipo y hacer que este plan se implemente con éxito. Espero que más personas con ideas afines se unan a nuestro equipo de documentación de Tencent y se unan. Explora y viaja, y finalmente espero que este artículo te sirva de inspiración ????

Supongo que te gusta

Origin blog.csdn.net/Tencent_TEG/article/details/108988820
Recomendado
Clasificación