Breakpoint debugging sharing and reuse practice of large front-end projects

Author: enoyao, Tencent engineers

background

As our project becomes larger and larger, we may need to maintain a lot of modules. Our Tencent Document Excel project has more than 10 large modules, and each large module has N small modules, and each large module has small modules. The main person in charge is following up on module issues.

This will lead to a big problem. In most cases, the person in charge of the module only pays attention to the problems of his own module, and does not know much about the specific problems of the modules of other persons in charge.

For example: when we have feedback from users that there is a problem with copy and paste, and we want to quickly locate the problem, we can only find the person in charge of the copy and paste module to deal with it. If the person in charge of the copy and paste module asks for leave, then other persons in charge When dealing with this problem, the solution cost will be very large, because other responsible persons may not be familiar with this module at all.

Another example: when we have a few new classmates and want him to quickly troubleshoot user feedback, we can only teach him our experience in debugging this module, and tell him the familiar pits, or organize them Show him the corresponding iwiki (generally low efficiency and no one sees it!), let him locate the problem slowly, so that every new student is familiar with the module, the cost of learning and maintenance will become more and more expensive , The larger the project, the more serious this situation will be!

So we have thought a lot about how to solve these problems, at least to make the module maintenance cost lower, become better to maintain and locate the problem.

Program

Because the above problem is really painful, we gradually explored a set of solutions in the crawling and rolling. Let's call it a shared and reused practical solution based on breakpoint debugging. There is a key word here is breakpoint Compared with being familiar to every developer, when we are front-end and module positioning problems, we inevitably use breakpoints to break some key areas of code operation. Here is an example:

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

The above code is when the user feedbacks a copy and paste problem, the person in charge who is familiar with the module knows that there is a problem with the external paste according to the user’s feedback, because he is familiar with the module, he will quickly display it in the browser console break point in the source code or manually inject debuggerkeywords to locate the user's problem step by step, he will first check the internal paste pasteFromOuterif triggered, then check function isShapePasteFromOuteris running successfully, the parameters and the parameters are correct, whether the code to go Crooked, go isImgPasteFromOuter.

Then after troubleshooting and repairing the problem, take a long sigh of relief. When you encounter the next problem, clean up the current debugging traces in the browser or code, and repeat the above series of actions again and again. I believe Most of the students are repeating the above similar actions every day when troubleshooting problems or even making needs. Can we consider saving these precious debugging traces, and when we or other students encounter similar module problems, we will take these The mental journey that condenses our blood and tears will automatically recur again?

code segment Record the debugger location
pasteFromInter 2 rows and 4 columns
isShapePasteFromOuter 256 rows and 89 columns
isImgPasteFromOuter 867 rows and 12 columns

For large projects, the time cost of each small bug debugging link is extremely huge, and it is also difficult to reproduce and reproduce. What we can do is to reuse similar ones when we encounter similar problems again. Debugging experience. There have been traces and experiences of injuries. When problems meet again, we should be more confident and calm.

So our first task has actually become to retain precious debugging links, that is, to retain countless days and nights, every breakpoint that pierces and stings deep in our hearts.

Plug-in

In the course of practice, we have tried countless methods. The first solution is to implement breakpoint retention based on a browser plug-in. Based on the interface provided by the Google Chrome plug-in development chrome.debugger, it is a message transmission method of the Chrome remote debugging protocol. chrome.debuggerYou can attach to one or more tabs to debug JavaScript. And use the debuggee to do plug-in communication based on sendCommand and onEvent. It allows us to debug the page in the plug-in. Many plug-ins and tools communicate with the browser console based on this protocol. This solution can only implement a remote debugging panel, which is similar to the debugging of the browser itself. The interface can load code and record breakpoints, and finally share these breakpoints.

This kind of solution experience will be worse. First, the debugging panel implemented by the plug-in itself cannot be as good as Google Chrome. Secondly, the plug-in needs to be developed and installed actively. The premise of sharing is that both parties need to install the corresponding plug-in, develop and promote costs are high, so personally do not recommend, but this does not mean that this program leads nowhere, because the plug-in can also be based on another realization, is the following debugfunction program.

debug function

DETAILED using function breakpoint debug(functionName) and undebug(functionName)methods, wherein the debug function is to functionName. We can be debug()inserted into the code (this method and console.log () statement is similar), it can also be called from DevTools console. debug()It is equivalent to setting a code line breakpoint in the first line of function.

Generally is used in the console, this method with the plug-in will have a better experience, because plug-in uses chrome.devtools.inspectedWindow.evala method with a browser interface can inject code into the console to perform in order to achieve help you automatically issued breakpoint function.

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

But careful students find that I use the debugfunction monitor is a global function window.xxxApi, so here sum up the experience, drawback of this approach is that if you use the console, it will look for the function in your context, so it Generally, it can only be used for global function management. If the function to be dotted is not in the context, you need to manually breakpoint to the scope of the target function, and then use the function to trigger it. If it is a closure function, there is no way, but the flaws are not hidden. , This method can help us quickly locate any global function, even if the code is confused, it can still be read quickly and add function breakpoints to you, so I suggest this solution as an alternative, in some cases Can play a miraculous effect!

AST injection

After experiencing the various pits above, let's briefly introduce a set of solutions we have implemented:

Our program is in fact before the function call chain solutions made on the basis of an improved, since we can develop their own code input debuggerkeyword to the code live off anywhere, why do not we put this tool to work?

First of all, we can use the state machine to tell the tool where we need to distribute the RBI location, similar to our commonly used whistle configuration table:

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 A state is described here, which is a behavior of distributing breakpoints, which is used to monitor that type of module, for example: copy and paste module, data layer module or data layer module

  • -f functionname -s codeThis state may be described herein specific behavioral characteristics such as: the pasteFromInterdistribution function breakpoints, and injected debuggercode.

In webpack, we can parse this configuration file in the two processes of loader or plugin. Here you can also use third-party libraries or regulars to parse the above status text. I was the loader to resolve this state table I in the global directory or locally within a module definition .debug.jsonfor writing state described above, a map object is then parsed out:

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

If you do not want to write the way the state machine configuration file, in fact, you can use a debug.jsonfile to describe the location of a breakpoint, this approach is simpler, cost parse json file is lower than many of the state machine's configuration file, json file The main fields involved here are the path of the code that needs to be detected, this convenient tool to locate the file, and then the name of the class or function that needs to be detected, this convenient tool to locate the location of the code, and the name of the detection item and the need to be detected The code, and a key key value:

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

Key may be defined herein relates to a clear point, such MessageCenter|function|1means is a function of a RBI inside of MessageCenter file module, to continue to improve in the future can also be written MessageCenter|class|1:12, means that a particular class of a certain dot position MessageCenter file inside the module, If the semantics of this key are richer, the subsequent distribution will be more accurate, and the positioning problem will be more efficient. The specifics can be defined according to the business scenario.

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

When we have the configuration file, we have to think about how to add debugging and detection code to the code without intrusion. We prefer to inject through AST, which can help us sort out the key parts of the code into a tree, such as erasing Colons, parentheses, semicolons, etc., allow us to focus on important nodes. After the above code is parsed, the following AST syntax tree will be obtained:

{
  "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": " 内部粘贴" }],
        }]
      }
    }]
  }
}

Probably the specific steps are as follows: parsing MessageCenter|function|1a string of this configuration parameters to obtain the function name, the module name, location information, etc., and then scans the code and syntax and lexical analysis, the syntax tree AST and obtained according to the function just parsed Name, module name, location information to match the AST tree node, add our debugging and detection code on it, and finally output the code that has been processed by us.

We all know the above principles. We can use plugins to implement it in the webpack tool. In plugins, we often use visitor mode, which means that when we access a certain path, we match it, and then this modification of the node, such as the above pasteFromInterfunction, it is a ClassMethod, the generated code will plugins AST tree visit, the visitor can match any corresponding lexical characteristics, here we can match all ClassMethodthen get the path corresponding to the node information, such as function names, function arguments, and a function of location, etc., to get the key information, we can carry out the function of the processing node, which is injected our commissioning and testing code or a direct injection debuggerto interrupt point.

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

Of course, the injection detection code is required to construct ClassMethoda similar structure, all we can with the @babel/typestools to quickly inject a piece of code, such as the simplest is to inject a debugger:

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

This will put one in a specific location of your matching path debugger, and your code source file itself has not been changed in any way, but a piece of code has been successfully merged to the specified location through the AST tree and the configuration file. Of course, the actual situation will be It is more complicated than expected, because it is possible that the location of the delivery is not a certain position in the function, it may be a certain position in the class function, a certain position in the closure function, so it must be compatible with various grammatical structures. Only by matching all the features of these functions in the AST can the code be delivered accurately, or take the function as an example, and list some of the situations that need to be considered:

  • FunctionExpression

These two writing methods need to be satisfied, otherwise the debugger will send the wrong position.

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

This general situation can be located in the following way, but if you want to be more precise, such as a private function, then you need to write a more precise accessor.

class xxx { xxx:(){ debugger } }
  • FunctionDeclaration

In addition to dealing with the writing of the above function expression, don't forget that the function also has the writing of the declaration definition, so this must be full.

function xxx() { debugger }
  • ArrowFunctionExpression

Finally, consider the writing of the down arrow function

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

Although in most cases the matching function can cover most of the scenes of the debugging code issued by the project, there will always be a fish in the net. For example, if some students want to inject the detection code before the class definition, they need to continue to write the corresponding accessor To obtain the path, and then distribute the corresponding detection code to the location, so you need to be familiar with various syntax and corresponding accessor types to achieve smoothly.

After the above transformation, we will get a new code in the final code (all the detection code has been injected), but this will trigger a new one. When we run this new code, all the detection codes above will be run again. This will break a lot of code areas that other module leaders don’t want to break, so in practice we need to distribute a detection code with a switch. Of course, the switch can actually be very simple, as follows:

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

We can use the require('@tencent/vdebugger')package a function that can be designed to read the configuration in a global variable or localstorage and other places, and then returns a Boolean value that determines whether the position debuggerwhere in order to debug convenience there are several small details that need attention, debuggerthis Key words have to separate a scope, so you can not write similar like this false || debugger, and require('@tencent/vdebugger')this function after reading the configuration inside the package which can be a evalmethod to perform detection code, so you can use callthe agency over the current scope, more convenient Go for debugging.

Of course, the actual situation may be more complicated than imagined. Take a simple example: because the distributed switch may be injected into some code that is packaged into the worker, the worker is used a lot in large projects, but the worker cannot read it. Document, window, these objects, although navigator, location and XMLHttpRequest can be used, but they cannot be controlled by means of localstorage reading configuration and other means, so you need to consider whether you need to distribute the debugging switch to the worker code. How to communicate the corresponding switch and other issues.

The simplest and rude is to filter when packaging the worker code.

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

Of course, if you need to distribute the switching effect worker, you need to implement a means of communication switch configured to read, the most common means of communication is based on postMessage, so that require('@tencent/vdebugger')the function that the switch module to accept the configuration of the main thread running code whereabouts worker Issue the command whether to execute the detection code and start the breakpoint.

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

Thinking

After implementing the above basic functions, we can continue to optimize many experiences. For example, we can also use the webpack plugin to implement incremental updates during local compilation. This can be achieved when we change the local configuration file, automatically distribute breakpoints and debug code, the logic is relatively simple, using the built-in apply cycle plugin library chokidarto monitor the change of the configuration file, and then compile the trigger, re-take the AST to debug code compiled together with the breakpoint code:

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

to sum up

There are not many debugging-related articles on this aspect, and many pits have been jumped along the way. Thank you for the support of the team members and make this plan successfully implemented. I hope that more like-minded people will join our Tencent documentation team and go together. Explore and travel, and finally I hope this article will give you some inspiration????

Guess you like

Origin blog.csdn.net/Tencent_TEG/article/details/108988820