Hello, ladies and gentlemen, welcome to the world of Quint’s White House (please don’t Sue me for infringing lines, for the sake of your publicity, T.T.). As a small front-end of a browser, WE recently received a demand: a foreign video website has a blank screen after our browser is opened. We hope to help locate the cause and propose a solution.

After replacing files with various captured packages from Fiddler, the problem was pinpointed as a bug in our browser’s strict mode. But browsers take too long to launch, and relying on businesses to push the other side to change the code is not a quick fix, so the ball is back in the poor little front end. Since the browser is your own, can you solve this problem by using injection in the other page?

The sample code looks like this:

<! doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, User - scalable = no, initial - scale = 1.0, the maximum - scale = 1.0, Minimum -scale=1.0"> <meta http-equiv=" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <script  src="1.js"></script> <script src="2.js"></script> <script src="error.js"></script> </body> </html>Copy the code

In this case, the solution we can think of is to replace error. Js on the page with ok. Js we uploaded to the CDN, so that the page is fine. However, injection is not a bullet. The client can only inject JS files into the file, and cannot directly modify the DOM content. This means that we cannot directly modify the SRC pointer. There seemed to be an impasse. However, let’s review the browser rendering process. Without adding defer and async properties, the browser will stop when it hits the

MutationObserver profile

MutationObserver gives developers the ability to react appropriately to changes in a DOM tree within a range. This API is designed to replace Mutation events introduced in the DOM3 event specification.

In fact, the introduction of MutationObserver by MDN is quite clear, but some of the details may not be easy to understand. At first glance, this API looks pretty complicated, but don’t worry, just keep two things in mind. We start by creating an observation instance through a constructor called MutationObserver(), which takes a function as a sole argument, const Observer = MutationObserver(function(){}). The next thing you need is a configuration object, MutationObserverInit, const Option = {}, don’t worry about the fancy stuff, it’s just a configuration. Then call the instance and pass in observer.observe(targetElement, option). See? See? Isn’t that easy?

However, for the front end, using the API without talking about compatibility is a bit of a bully, so we need to see how compatible MutationObserver is with browsers:

See the basic are forgive green, especially mobile terminal, can rest assured to use. Note that older browsers require a prefix, namely WebKitMutationObserver or MozMutationObserver. Since my browser supports this API, I won’t add the prefix. Otherwise, we can refer to the example of MDN:

var observer = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
Copy the code

This ensures compatibility.

A brief analysis of MutationObserverInit objects

Constructors and the functions passed in should have been introduced first, but on reflection, the configuration object is easier to understand first. As mentioned earlier, the MutationObserver can monitor DOM changes, but consider that there are many types of DOM changes, such as text changes, property changes, and so on. Wouldn’t it be a waste of browser resources if the API were to monitor all changes? It is best practice to monitor the scope with control, hence the MutationObserverInit configuration object.

MutationRecord can be configured a lot, at first glance will really meng force, but after a good induction, found that is actually very clear. We came once from the Lord. The first is the type of monitoring, which can be divided into three categories:

ChildList, which stands for observing the change of the target node idea node; Attributes, which stands for observing the changes of the target node attributes; CharacterData: Stands for observing the changes when the target node is a text node or a comment node.

CharacterData will only work if the target node to be observed is specified as a text, comment, etc. As in Vue source code. .

Here’s an example:

const p = document.querySelector('p'); const childListBtn = document.querySelector('.childList'); const characterDataBtn = document.querySelector('.characterData'); childListBtn.addEventListener('click', function() { p.innerHTML = 'childList'; }) characterDataBtn.addEventListener('click', function() { p.firstChild.data = 'characterDataBtn'; }) / /... Observe relevant codeCopy the code

If the target node you are looking at is the P tag, when its text changes, it triggers a change of type childList, that is, child node changes. The characterData type change will only be triggered if the observed target node is set to p.firstChild and the properties in p.firstChild are changed. I want you to pay attention to this detail.

After the classification, there are only a few options left to enhance the above three types:

Subtree, which means to observe not only the child nodes, but also all the descendants of the target node. All three types can be enhanced. AttributeOldValue indicates whether the value of the attribute before the changed attribute node needs to be logged. This is used to enhance attributes. AttributeFilter, an array option whose only value is not a Boolean, is observed only when the names of the attributes contained in the array change, also enhancing attributes; CharacterDataOldValue, which represents whether the text prior to the characterData node that changed needs to be written down. This is used to enhance attributes.

Note that the default value for all three categories and enhanced options is undefined, and the enhanced options must be set to true, otherwise the browser will report an error.

This completes the configuration object, MutationObserverInit, and lets take a look at the MutationObserver constructor and its key callback function.

The constructorMutationObserver

I lied when I said that the MutationObserver constructor is important. The important thing is that the constructor understands the argument it takes, the callback function. The callback function is passed two arguments when executed. The second argument, which is easier to understand, is passed in the instance that comes out of new. The first argument is an array, where each item is an object: MutationRecord, which we’ll take a closer look at.

The MutationRecord object contains a number of properties, mainly to show what has happened to the DOM node:

I’m going to be a little lazy here and go straight to the instructions in the MDN document. The documentation is very detailed, but there are a few details that need to be noted. AddedNodes and removedNodes are of type NodeList, which means that it is not a true array, so be careful when using array methods. Also, it’s more important to understand why the first argument to a callback function is an array. Logically, when a node changes, a MutationRecord object should be sufficient to describe its change. In my opinion, this sentence is both true and false in MutationObserver. The following understanding is only my personal opinion, and I also invite dalao’s advice.

What MutationObserver does is not compare the changes in the DOM after a function has executed, and pass in the relevant changes to the callback function when it detects the changes. Instead, it “logs” the changes in the DOM. This may seem confusing, but consider the following example:

<p>1111</p>
<button>click</button>

<script>
    function obFn(changeArr) {
      console.log(changeArr.length)
    }

    const observer = new MutationObserver(obFn);

    const option = {
      childList: true,
      subtree: true,
    };
    const p = document.querySelector('p');
    observer.observe(p, option);

    const btn = document.querySelector('button');
    btn.addEventListener('click', function() {
      p.innerHTML = '<span>1</span><span>2</span><span>3</span>';
    })
</script>
Copy the code

When the button is clicked, the console prints 1. But when the BTN event binding is changed to:

btn.addEventListener('click', function() {
  const a = document.createElement('a');
  a.innerHTML = '<span>123</span>';
  const a1 = a.cloneNode();
  const a2 = a.cloneNode();
  p.appendChild(a);
  p.appendChild(a1);
  p.appendChild(a2);
})
Copy the code

Click the button and the console prints out 3! In the first example, the

operation is recorded only once: p.innerhtml = ‘123‘; , so the length of the change array is 1, and the length of the addedNodes in the change object MutationRecord is 3. In the second case, the

operation is recorded three times, so the length of the change array is 3, and the length of the addedNodes in MutationRecord for each item in the array is 1.

With this comparison, you can see why MutationObserver is a “record” of DOM changes, and therefore the first argument to the callback is an array, rather than an object that only records the back and forth changes!

Observe, which accepts two arguments: observe the DOM node, and the MutationObserverInit configuration object. Disconnect, stops observing the DOM node; TakeRecords, empties the observer’s record queue and returns its contents.

summary

After writing for a long time, I feel as if I am off topic. Finally, I attach the code to solve the problem:

<! doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, User - scalable = no, initial - scale = 1.0, the maximum - scale = 1.0, Minimum -scale=1.0"> <meta http-equiv=" x-UA-compatible "content=" IE =edge"> <title>Document</title> <script> function observerFn(changeArr, observer) { changeArr.forEach((obj) => { const addNodes = [...obj.addedNodes]; addNodes.forEach((node) => { if (node.nodeName === 'SCRIPT' && /error/.test(node.src)) { node.src = 'ok.js'; observer.disconnect(); } }) }) } const observer = new MutationObserver(observerFn); const option = { childList: true, subtree: true, } observer.observe(document.documentElement, option); </script> </head> <body> <script src="1.js"></script> <script src="2.js"></script> <script src="error.js"></script> </body> </html>Copy the code

A few tweaks to the code above can actually prevent some hijackings. As long as the JS file is not in the whitelist, change its SRC and upload it to your own server, so that you can record what hijacked our web page, which is also a stretch of thought. In addition, MutationObserver’s ability to record DOM changes is so powerful that there are countless application scenarios waiting to be explored and developed.

These are my generalizations of MutationObserver, which I hope will help you. Improper place also please do not hesitate to give advice!