preface

As a front-end development, the most commonly used browser is Chrome, developer tools for Chrome is the most intimate tools. But if you look at the Console object output, you’ll notice something strange:

For example, we define an object as follows:

var a = { prop1: 1 }
Copy the code

At this point we print the object and print a folded object as follows:

At this point we modify the object and print it

a.prop1 = 2
Copy the code

In this case, the output is also a default decanted object, as follows:

But at this point, we click on the output object to expand, and see that the output has also been modified

Then we modify the object again:

a.prop1 = 3
Copy the code

When you look at the output of the first time, you will find that even if you click expand again to close the object, the value of the object will not be updated. The value of the object will remain the same as the value of the object when it was first expanded:

For the second output, when we click expand, we get the latest object data:

Why is that?

Log output module logic of Console

In fact, Chrome’s developer tools module interaction is actually a front-end application, like any other front-end development page, we can deploy this front-end application separately, and then analyze

Note, if you want to learn more about how to deploy the front end yourself, you can go to my article, “Remote Debugging principles and Remote Debugging Implementation solutions in intelligent Scenarios”

The developer tool, a visual front-end application, actually interacts with the DEBUGGING backend (where the code actually executes, such as the V8 engine in the browser) through the message channel, or WebSocket

This connection can be established by adding the WS parameter to the entry HTML access of the developer tool front-end application. Open the Developer tool and we can see the interface

Everything we do in the developer tools is just a front-end visual interaction, and all the data and code execution is in the debug back end. These log outputs, in fact, through the websocket message channel mentioned above, the debugging back end tells the developer tools what kind of data the front-end application does.

As the above output object is actually sent by debugging backend corresponding messages, Runtime.consoleAPICalled

Where type is used to indicate which method of console is used, Optional values include log, DEBUG, info, error, warning, dir, dirxml, table, trace, clear, startGroup, startGroupCollapsed, endGroup, assert, profile, profileEnd, count, timeEnd

StackTrace is used to indicate the call stack

The most critical parameter is the arGS parameter, which is the final log parameter information to be output. Its definition is as follows:

After analyzing the above message, args has only one parameter, which is an object of type, but does not have specific object attribute information, including an objectId attribute

ObjectPreview (); overflow (); ObjectPreview (); overflow (); , use Properties to get preview property information

At this point, we can see that the Console module in the developer tools, when it shows the output of the object, doesn’t actually get all the properties of the final object, just a simple “preview”.

So what happens when we click the log object to expand?

The front end sends two messages, both belonging to Runtime.getProperties, to query the debugger back end for the properties of the object as follows:

After receiving this message, the debugging backend returns all the properties of the object according to the parameter configuration

There are two messages because ownProperties and accessorPropertiesOnly query parameters are different and are used to query stereotype properties and object properties respectively

At this point, we know that when we click expand for the first time, it actually sends the interface to query all the property information of the object at this time and renders it to the page. Therefore, it expands and renders the property information of the object at that time.

When the object information is expanded again later, because the rendering is finished, the front-end implementation of the developer tool caches the rendering result and does not trigger the interface query again.

Better log export

If you right-click under the Console panel, a menu will pop up with an option to export logs that are currently displayed on the Console

If we try to export and look at the exported log information, we find that the exported information about objects is not comprehensive, as follows:

Log. ts:33 {prop1: "prop1", prop2: "prop2", prop3:3, prop4:4, prop5: Array(5),... }Copy the code

This is… Depending on the exported log information to troubleshoot the problem, obviously does not meet the requirements. Let’s take a look at why this is the case from the source point of view.

Developer tools source, need from the Chromium project, compile Chromium DevTools Frontend, here I directly share my compiled developer tools front-end project: full source see: github.com/tongyuchan/…

I’m using the Chromium /4400 branch, and the console source is basically in the console directory

├ ─ ─ ConsoleContextSelector. Js ├ ─ ─ ConsoleFilter. Js ├ ─ ─ ConsolePanel. Js ├ ─ ─ ConsolePinPane. Js ├ ─ ─ ConsolePrompt. Js ├ ─ ─ Consolesidebar.js ├── ConsoleView.js ├─ ConsoleView.js ├─ ConsoleView.js ├─ ConsoleView.js ├─ ConsolesideBar.js ├─ ConsoleView.js ├─ ├─ console.js ├─ console_module.jsCopy the code

Consoleview. js is the core file of the Console panel, and the ConsoleView class defined in this file is the core code

From constructor, various modules are registered, including the toolbar, the left sidebar module, and the right-click menu module

// devtools-frontend/chromium-4400/console/ConsoleView.js export class ConsoleView extends UI.Widget.VBox { constructor() { // ... this._viewport = new ConsoleViewport(this); this._messagesElement = this._viewport.element; / / listen for an event this. _messagesElement. AddEventListener (' contextmenu 'this. _handleContextMenuEvent. Bind (this), false); } _handleContextMenuEvent(event) { const contextMenu = new UI.ContextMenu.ContextMenu(event); / /... // Add the save as option and bind the contextmenu.savesection ().appendItem(common.uiString.uiString ('Save as... '), this._saveConsole.bind(this)); } async _saveConsole() {const url = /** @type {! SDK.SDKModel.Target} */ (SDK.SDKModel.TargetManager.instance().mainTarget()).inspectedURL(); const parsedURL = Common.ParsedURL.ParsedURL.fromString(url); const filename = Platform.StringUtilities.sprintf('%s-%d.log', parsedURL ? parsedURL.host : 'console', Date.now()); const stream = new Bindings.FileUtils.FileOutputStream(); const progressIndicator = new UI.ProgressIndicator.ProgressIndicator(); ProgressIndicator. SetTitle (Common) UIString) UIString (' Writing the file... ')); progressIndicator.setTotalWork(this.itemCount()); /** @const */ const chunkSize = 350; if (! await stream.open(filename)) { return; } this._progressToolbarItem.element.appendChild(progressIndicator.element); let messageIndex = 0; while (messageIndex < this.itemCount() && ! progressIndicator.isCanceled()) { const messageContents = []; let i; for (i = 0; i < chunkSize && i + messageIndex < this.itemCount(); ++i) { const message = /** @type {! ConsoleViewMessage} */ (this.itemElement(messageIndex + i)); Messagecontents.push (message.toexportString ())); } messageIndex += i; await stream.write(messageContents.join('\n') + '\n'); progressIndicator.setWorked(messageIndex); } stream.close(); progressIndicator.done(); }}Copy the code

Let’s look again at the definition of message.toexportString ()

// devtools-frontend/chromium-4400/console/ConsoleViewMessage.js
toExportString() {
  const lines = [];
  const nodes = this.contentElement().childTextNodes();
  const messageContent = nodes.map(Components.Linkifier.Linkifier.untruncatedNodeText).join('');
  for (let i = 0; i < this.repeatCount(); ++i) {
    lines.push(messageContent);
  }
  return lines.join('\n');
}
Copy the code

Nodes here are actually DOM elements

That eventually every text again experienced Components. Linkifier. Linkifier. UntruncatedNodeText methods of treatment

// devtools-frontend/chromium-4400/components/Linkifier.js static _appendHiddenText(link, String) {const ellipsisNode = UI. UIUtils. CreateTextChild (link. CreateChild (' span ', 'devtools - link - ellipsis'),'... '); textByAnchor.set(ellipsisNode, string); } static untruncatedNodeText(node) { return textByAnchor.get(node) || node.textContent || ''; }Copy the code

And then you actually end up callingnode.textContentMethod, which directly retrieves the text value of the DOM element. No wonder you can’t export the actual data, just the text content of the presentation element

So what if we want an export function that can export log objects?

Referring to the analysis of log input logic in the first part, we can write an export method by ourselves. According to the output log information, if more information is needed, such as objects, we can send CDP messages to query more detailed information output.

/ / devtools - frontend/chromium - 4400 / console/ConsoleView. Js export class ConsoleView extends the UI. The Widget. The VBox {/ / object properties, Added method async _getProperties(obj, objectId) {const resObj = {}; // Send CDP messages. Const {result, const {result, internalProperties } = await obj._runtimeModel._agent.invoke_getProperties({ accessorPropertiesOnly: false, generatePreview: true, objectId: objectId, ownProperties: true, }) const allProperties = [...result, ...(internalProperties || [])]; for (let i = 0; i < allProperties.length; i++) { const item = allProperties[i]; resObj[item.name] = item.value.value || item.value.description; try { if (item.value.type === 'object') { resObj[item.name] = await this._getProperties(obj, item.value.objectId); } } catch (err) {} } return resObj; } async _saveConsole() {const url = /** @type {! SDK.SDKModel.Target} */ (SDK.SDKModel.TargetManager.instance().mainTarget()).inspectedURL(); const parsedURL = Common.ParsedURL.ParsedURL.fromString(url); const filename = Platform.StringUtilities.sprintf('%s-%d.log', parsedURL ? parsedURL.host : 'console', Date.now()); const stream = new Bindings.FileUtils.FileOutputStream(); const progressIndicator = new UI.ProgressIndicator.ProgressIndicator(); ProgressIndicator. SetTitle (Common) UIString) UIString (' Writing the file... ')); progressIndicator.setTotalWork(this.itemCount()); /** @const */ const chunkSize = 350; if (! await stream.open(filename)) { return; } this._progressToolbarItem.element.appendChild(progressIndicator.element); let messageIndex = 0; // while (messageIndex < this.itemCount() && ! progressIndicator.isCanceled()) { // const messageContents = []; // let i; // for (i = 0; i < chunkSize && i + messageIndex < this.itemCount(); ++i) { // const message = /** @type {! ConsoleViewMessage} */ (this.itemElement(messageIndex + i)); // // The core here is that each message object calls the toExportString method // Messagecontents.push (message.toexportString ()); // } // messageIndex += i; // await stream.write(messageContents.join('\n') + '\n'); // progressIndicator.setWorked(messageIndex); Const msgArr = this._consolemessages; for (let i = 0; i < msgArr.length && ! progressIndicator.isCanceled(); i++) { const item = msgArr[i]; Const MSG = item.consolemessage (); // Hidden logs are not printed. Non-log commands are not exported. this._shouldMessageBeVisible(item) || ["log", "error", "warning", "info", "debug"].indexOf(msg._type) === -1) { continue; } const params = [...msg.parameters]; / / parse log stack const path = ` ${MSG. Url. The split ('/'). The pop (). The split ('? ')[0]}: ${msg.line + 1}`; try { const textArr = []; while (params.length) { const currentParam = params.shift(); // Function, re, date object, Symbol, Error take description display, The base type of the value to show the let val. = currentParam value | | currentParam. Description the if (currentParam. Type = = = 'string') {/ / style not print / / Const hasStyle = currentParam.value.match(/%c/g); let ignoreLength = hasStyle ? hasStyle.length : 0; while (ignoreLength) { params.shift(); ignoreLength--; } val = val.replace(/%c/g, ''); If (['Array', 'Object', 'Number', 'String', 'Boolean'].indexOf(currentParam.className) > -1) { val = await this._getProperties(msg, currentParam.objectId); val = JSON.stringify(val) } textArr.push(val); } await stream.write(`${path} ${textArr.join(' ')}\n`) } catch (err) { await stream.write(`${path} ${JSON.stringify(params)}\n`) } progressIndicator.setWorked(i) } stream.close(); progressIndicator.done(); }}Copy the code

The exported log information is as follows:

log.ts: 33 {"prop1":"prop1","prop2":"prop2","prop3":3,"prop4":4,"prop5":{"0":5,"1":5,"2":5,"3":5,"4":5,"length":5},"prop6":true,"pr op8":{"prop8_prop1":1},"prop9":{},"prop10":10}Copy the code

While I’m still a bit rough with arrays and strings wrapped around objects, if you want more readable processing, you can do further processing by type.

How to add custom functions

How to add custom functionality to the Console module? For example, the above log export function, if you do not want to change the default behavior, but want to have a log export function, what can be done in the toolbar? Implement a button by yourself

// devtools-frontend/chromium-4400/console/ConsoleView.js export class ConsoleView extends UI.Widget.VBox { constructor() { // ... // The right part of the Toolbar, such as const rightToolbar = new ui.tooltool.toolbar (", this._consoleToolbarContainer); / / add divider rightToolbar. AppendSeparator (); / / hide the log information rightToolbar. AppendToolbarItem (enclosing _filterStatusText); / / set button rightToolbar. AppendToolbarItem (enclosing _showSettingsPaneButton); }}Copy the code

Corresponding to the page:

We can also implement a component ourselves and add it to the toolbar on the right. The concept of Web Components has been around for a long time, and developer tools use this technology almost entirely.

I will not expand how to customize a component here, but share the existing CounterButton component implementation

// devtools-frontend/chromium-4400/ui/components/Icon.js import * as LitHtml from '.. /.. /third_party/lit-html/lit-html.js'; const isString = (value) => value ! == undefined; export class Icon extends HTMLElement { constructor() { super(... arguments); this.shadow = this.attachShadow({ mode: 'open' }); this.iconPath = ''; this.color = 'rgb(110 110 110)'; this.width = '100%'; this.height = '100%'; } set data(data) { const { width, height } = data; this.color = data.color; this.width = isString(width) ? width : (isString(height) ? height : this.width); this.height = isString(height) ? height : (isString(width) ? width : this.height); this.iconPath = 'iconPath' in data ? data.iconPath : `Images/${data.iconName}.svg`; if ('iconName' in data) { this.iconName = data.iconName; } this.render(); } get data() { const commonData = { color: this.color, width: this.width, height: this.height, }; if (this.iconName) { return { ... commonData, iconName: this.iconName, }; } return { ... commonData, iconPath: this.iconPath, }; } getStyles() { const { iconPath, width, height, color } = this; const commonStyles = { width, height, display: 'block', }; if (color) { return { ... commonStyles, webkitMaskImage: `url(${iconPath})`, webkitMaskPosition: 'center', webkitMaskRepeat: 'no-repeat', webkitMaskSize: '100%', backgroundColor: `var(--icon-color, ${color})`, }; } return { ... commonStyles, backgroundImage: `url(${iconPath})`, backgroundPosition: 'center', backgroundRepeat: 'no-repeat', backgroundSize: '100%', }; } render() { // clang-format off LitHtml.render(LitHtml.html ` <style> :host { display: inline-block; white-space: nowrap; } </style> <div class="icon-basic" style=${LitHtml.Directives.styleMap(this.getStyles())}></div> `, this.shadow); // clang-format on } } if (! customElements.get('devtools-icon')) { customElements.define('devtools-icon', Icon); }Copy the code

However, it is important to note that there are some special points about event propagation when Web Components use Shadow DOM:

The element event propagation mechanism inside the Shadow DOM is the same as normal DOM, and events can be safely registered. But the target attribute in the event is a little different.

When the Event is propagated within the Shadow DOM, the event. target accurately reflects the element that triggered the Event. Once the Event propagated reaches Shadow Host, the event. target changes to the Shadow Host element in subsequent events. Perhaps this is also a protection mechanism, as elements inside the Shadow DOM cannot be accessed externally.

Therefore, it should be noted that if the Event delegation mechanism is adopted outside the Shadow DOM, the target Event elements cannot be accurately determined through event. target

What do you do in this case?

1) Method 1: Add two event delegates, one to handle the logic bubbling from Shadow Host and one to handle the logic bubbling inside Shadow DOM

2) Method two: Using Event.composedPath(), returns an array of EventTarget objects representing the object on which the event listener will be called. Only when mode is open can the element inside the Shadow DOM be retrieved. Otherwise, the Shadow Host element is the first element

Node. textContent vs. Node. innerText vs. Node. innerHTML

TextContent vs. Node.innerText vs. Node.innerHTML is the default API used by the developer tool for exporting logs.

Node. textContent: represents the textContent of a node and its descendants

Node. innerText: Property represents the “rendered” text content of a node and its descendants

Node.innerhtml: Sets or gets the descendant of the element represented by the HTML syntax

The main confusion here is node.textContent vs. Node.innerText

Note: innerText can easily be confused with Node.textContent, but there are important differences between the two properties. In general, innerText manipulates rendered content, while textContent does not.

For example, if a node in a node is hidden by display: None, the innerText does not contain the node text, but the textContent does.