Abstract

✨ The long reading takes about 10 minutes

✨ It takes more than an hour to follow

✨ about 100 lines of code

Some time ago, I planned to write a fake console for the mobile terminal, which can be used to view the output of the console. This function has been completed

However, I later learned that there was a Tencent team’s project vConsole for reference and found that its functions were much richer. You can see Network panel and so on

It’s a neutered panel but you can still see some important information about the network

So I also want to add this Network panel function

There are a couple of ways to do that and one of the ways to do that is to encapsulate an Ajax but that’s not really practical and having everyone else use your own encapsulating Ajax is not very friendly to introduce in the middle of a project and you have to change a lot of code

So this scheme is not desirable

The scheme selected then intercepts the methods below the console just as it did here implementing the Console panel

Intercepting a method on a Console object actually overrides its method

Console. log, for example, is essentially a rewrite of the log method to do whatever you want to do before actually executing the log

Train of thought

The first thing that comes to mind in this section is that the XMLHrrpRequest object has a global hook function and I looked up the document and didn’t seem to find anything about it

Later searched some other way to achieve most is spicy chicken station do not know where to climb from the content of nothing valuable things

Then I found a repository on Github (Ajax-hook)

This library is designed to intercept global Ajax requests using methods or attributes of the same name as hooks

Part of the content refers to his ideas of realization

In fact, the implementation is a lot like a custom console that still runs on the native XHR object at the bottom but overrides the XMLHttpRequest object at the top

The general implementation idea can be listed in the following steps

  1. Keep the native XHR objects
  2. willXMLHttpRequestObject is set to the new object
  3. The methods of the XHR object are overridden and put into the new object
  4. Override the property and put it into the new object

The main thing overriding methods do is provide a hook to execute the custom content before executing the native method

Override properties are basically event-triggered properties like onReadyStatechange

There are also other pits where many properties on XHR objects are read-only (this is also known through Ajax-hooks)

If you change a read-only property in the hook, it doesn’t work further down because it didn’t work

But now that we have the general idea let’s try to implement it

implementation

Why don’t you think about how to use it before you implement it so that you have a general direction of how to implement it

I’ll call him any-xhr first

Use new AnyXHR() to create an instance

There are two kinds of hooks that can be added in constructors: before and after

Just like this, the constructor takes two arguments as objects: the first is the hook that executes before the call and the second is after the call

The key in the object corresponds to the native XHR methods and attributes

	let anyxhr = new AnyXHR({
		send: function(... args) { console.log('before send');
		}
	}, {
		send: function(... args) { console.log('after send'); }});Copy the code

The constructor

So now we have our goal and we’re going to implement it as we thought we’re going to instantiate an object using the new keyword and we’re going to create a class using the constructor keyword or the class keyword

The class keyword is used here

	class AnyXHR {

		constructor(hooks = {}, execedHooks = {}) {
			this.hooks = hooks;
			this.execedHooks = execedHooks;
		}
		
		init() {// Initialize operation... } overwrite(proxyXHR) {// Handle overwriting properties and methods under XHR objects}}Copy the code

For now, I’m just going to focus on constructors

The constructor accepts two objects that represent the pre-execution hook and the post-execution hook corresponding to the hooks and execedHooks properties attached to the instance

Follow these steps to preserve the native XHR object and throw this step into the constructor

And then we’re going to do some initialization where we’re going to rewrite the XHR object

	constructor(hooks = {}, execedHooks = {}) {
+		this.XHR = window.XMLHttpRequest;

		this.hooks = hooks;
		this.execedHooks = execedHooks;
		
+		this.init();
	}
Copy the code

The init method

So what you do in init is you override XMLHttpRequest every method and property of an XHR instance

So when we do new XMLHttpRequest(), we’re going to be able to rewrite our own XHR instance

	init() {
		
		window.XMLHttpRequest = function() {}}Copy the code

I’m going to assign XMLHttpRequest to a new function like this so that XMLHttpRequest is no longer the original one and the user is going to have global access to this new function that we wrote ourselves

Then it’s time to implement methods and properties that override native XHR instances like Open, onload, and Send

But how do I rewrite this so that I can use send and open after a new XMLHttpRequest, which is now an empty function

In fact, as mentioned earlier, overriding methods basically provide a hook to execute the custom content before executing the native method

To implement native methods, we still need to use native XHR objects

Take the Open for example

We need to create a new native XMLHttpRequest object as well as the user new XMLHttpRequest

And then he’s going to attach a send method to his overwritten instance of XMLHttpRequest and what he’s doing is he’s going to execute his own custom content and then he’s going to execute the open method on our new instance of the preserved XMLHttpRequest object and pass the parameters

It sounds a little convoluted and you can draw a picture and savor it

	init() {+let _this = this;		
		window.XMLHttpRequest = function() { + this._xhr = new _this.XHR(); // Mount a reserved native XHR instance on the instance + this.overwrite(this); }}Copy the code

This creates a preserved native XMLHttpRequest instance internally when the user new XMLHttpRequest

For example, when the user calls send, we do what we want to do first and then call this._xhr.send to execute the send method of the native XHR instance

So once we’re done with that, we’re going to go to the Overwrite method and what this method does is we’re going to rewrite every method and property that we talked about in the last sentence and we’re just tinkering with it before and after execution

So when we call _this.overwrite we need to pass this in and this in this case refers to the instance after the new XMLHttpRequest

Properties and methods are attached to the instance for the user to call so we pass this in for convenience

You can’t use arrow functions when overwriting window.xmlHttprequest with a new function. Arrow functions can’t be used as constructors, so I’m going to use this here

Overwrite method

To override methods and properties, you need to override send, open, responseText, onLoad, onReadyStatechange, etc

Should that be implemented one by one… Certainly not realistic

The methods and properties to override can be obtained by facilitating a native XHR instance

The native XHR instance has been retained on the _xhr attribute of the instance of the overridden XMLHttpRequest object

So by iterating through the native XHR instance of _xhr, you can get the keys and values of all the methods and attributes that need to be overridden

	overwrite(proxyXHR) {
+		for (let key in proxyXHR._xhr) {

+		}
	}
Copy the code

The proxyXHR parameter here points to a modified instance of XMLHttpRequest

Methods and properties are attached to instances

Now to proxyXHR under the _xhr attribute traversal can get his following all traversable attributes and methods

Then you can differentiate between methods and properties and do different things

	overwrite(proxyXHR) {
		for (let key in proxyXHR._xhr) {
		
+			if (typeof proxyXHR._xhr[key] === 'function') {
+				this.overwriteMethod(key, proxyXHR);
+				continue; + } + this.overwriteAttributes(key, proxyXHR); }}Copy the code

Typeof determines whether the current traversal is a property or a method and if so overwriteMethod is called to rewrite the method

If it is an attribute, rewrite the attribute using overwriteAttributes

You pass the traversed property and method names along with the modified XMLHttpRequest instance

So let’s implement these two methods

overwriteMethod

Add this method to the class

Class AnyXHR {+ overwriteMethod(key, proxyXHR) {Copy the code

What this method does is override the method in the native XHR instance

Actually, it’s not strictly strict to call this operation a rewrite

The send method, for example, does not modify the send method in the native XHR instance. The process of writing a new method that is attached to the XHR instance instead of the native XHR instance to perform the final SEND is still calling the native SEND method. It just does two more things before and after the call

So this is a wrapper for each method

	overwriteMethod(key, proxyXHR) {
+		let hooks = this.hooks;	
+		letexecedHooks = this.execedHooks; + proxyXHR[key] = (... args) => { + } }Copy the code

First, keep hooks and execedHooks, which will be used more frequently

Then we attach a method of the same name to the new XHR instance, for example, the native XHR has a send traversal to send, so we attach a SEND to the new XHR instance instead of the native SEND method

When a method is called it gets a bunch of arguments thrown in by the JS engine (or browser) and then grabs them together with the rest of the arguments to form an array that can be passed in when you call the hook or the native method

So what exactly does it do

There are really three steps

  • Executes the hook if the current method has a corresponding hook
  • Execute the corresponding method in the native XHR instance
  • See if there are any hooks that need to be executed after the method corresponding to the native XHR instance executes
	overwriteMethod(key, proxyXHR) {
		let hooks = this.hooks;	
		letexecedHooks = this.execedHooks; proxyXHR[key] = (... Args) => {+ // Execute hook + if the current method has a corresponding hookif (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {+return; Const res = proxyxhr._xhr [key]. Apply (proxyxhr._xhr, args); + execedHooks[key] && execedHooks[key].call(proxyxhr._xhr, res); +returnres; }}Copy the code

The first step is to determine if the current method has a corresponding hook function

The hooks object holds all the hook functions or methods that need to be intercepted after we execute new AnyXHR(..). It came in at the time

If it does, it executes and passes the argument to the hook. If the hook function returns false, it stops going down so that it intercepts

Otherwise, go down and execute the corresponding method in the native XHR instance

The native XHR instance is in the _xhr property so it can be accessed by proxyxhr._xhr [key] and pass in the parameters with apply and catch the return value

Then take the third step

See if there are any hooks that need to be executed after executing the native XHR method and then execute them and pass in the return value

It would then be nice to return the reverse of the method executed for the native XHR instance

To this method of proxy, interception is complete to try

OverwriteAttributes (key, proxyXHR); This line

First debugging

Although there is not much code up to now, it is still very tired not to bypass all of a sudden. Let’s get a glass of water and have a rest

The full code so far is below

	class AnyXHR {

		constructor(hooks = {}, execedHooks = {}) {
			this.XHR = window.XMLHttpRequest;
	
			this.hooks = hooks;
			this.execedHooks = execedHooks;
			
			this.init();
		}
		
		init() {
			let _this = this;		
			window.XMLHttpRequest = function() { this._xhr = new _this.XHR(); // Mount a reserved native XHR instance on the instance _this.overwrite(this); } } overwrite(proxyXHR) {for (let key in proxyXHR._xhr) {
			
				if (typeof proxyXHR._xhr[key] === 'function') {
					this.overwriteMethod(key, proxyXHR);
					continue;
				}
	
				// this.overwriteAttributes(key, proxyXHR);
	
			}
		}
		
		overwriteMethod(key, proxyXHR) {
			let hooks = this.hooks;	
			letexecedHooks = this.execedHooks; proxyXHR[key] = (... Args) => {// Execute the hook if the current method has a corresponding hookif (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
					return; } // execute the corresponding method in the native XHR instance const res = proxyxhr._xhr [key]. Apply (proxyxhr._xhr, args); ExecedHooks [key] && execedHooks[key].call(proxyxhr._xhr, res);returnres; }}}Copy the code

Try the first debugging

	new AnyXHR({
		open: function() { console.log(this); }}); var xhr = new XMLHttpRequest(); xhr.open('GET'.'/abc? a=1&b=2'.true);
	
	xhr.send();
Copy the code

You can go to the console and see if there’s any output and you can look at the object and the _xhr property and sit down and compare it

OverwriteAttributes method

This method is a little bit more complicated to implement and a little bit more convoluted

One idea might be why do we listen or do we delegate or do we provide hooks for properties is that useful

Properties like responseText are not actually targets

The goal is to wrap a layer around properties like onReadyStatechange and onLoad

These properties are kind of like events or events

They need to be manually assigned and at the corresponding moment the hooks that native XHR provides are automatically called

Things like Send and open can be intercepted when a request is sent

Onreadystatechange and onLoad can be used to intercept requests that the service responds to

So it is necessary to wrap these attributes

Now that you know why you need to wrap the property the question is how do you wrap the property

Let’s do onload for example

xhr.onload = function() {... };Copy the code

We should respond when the user assigns something like this

Capture the assignment process

Check to see if there are any onload hooks in the hooks array

If so, execute the hook before executing the onload of the native XHR instance

What about general properties like responseType when the problem comes up

These attributes are then left out and directly attached to the new XHR instance

Again, the question is how do you distinguish between ordinary properties and event-like properties

In fact, if you look at it, you can see that the on property is an event-like property

So to sum up

  • See if the attribute starts with on
  • If you don’t just hook it up
  • If so, see if there are any hooks to execute
  • If so, wrap the hook first and then the ontology
  • If there is no accountability directly assign the value to mount

Logic clear is not found very simple

All right, let’s do it

?? How do you listen for users to assign values to properties like onload?

You can stop and think about it

This is where you can use the getters and setters provided in ES5

This knowledge point is certainly very familiar not familiar can turn over MDN

These two methods allow you to listen to the user’s assignment and value of a property and do some extra things

How do I set the get/set method for the property I want to insert

ES5 provides the Object.defineProperty method

You can define properties for an object and you can specify its property descriptor which describes whether a property is writable or enumerable and its set/get and so on

It is also possible to define a literal get/set method for an attribute

So let’s just implement this method

	overwriteAttributes(key, proxyXHR) {
		Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR));
	}
Copy the code

So this is just one line of code where you attach a property to your implementation XHR instance with object.defineProperty and the property name is the key that’s passed in and then you use the setProperyDescriptor method to generate the property descriptor and pass the key and the instance in

The descriptor will generate the get/set method which is what happens when this property is assigned and evaluated

SetProperyDescriptor method

Again, add this method to the class

	setProperyDescriptor(key, proxyXHR) {
	
	}
Copy the code

You can get the attribute name (key) you want to add and the XHR object instance you implement

Properties are attached to this instance

	setProperyDescriptor(key, proxyXHR) {
+		let obj = Object.create(null);
+		let _this = this;
		
	}
Copy the code

The property descriptor is actually an object

Here we use Object.create(null) to generate a perfectly clean Object in case any of the messy properties become descriptions by mistake

And I’m going to keep this

And then we implement set

	setProperyDescriptor(key, proxyXHR) {
		let obj = Object.create(null);
		let _this = this;
		
+		obj.set = function(val) {

+		}
	}
Copy the code

The set method is called when the property is assigned (e.g. Obj.a = 1) and it takes an argument that is the assigned value (the value to the right of the equals sign).

Then inside you can do the steps listed earlier

  • See if the attribute starts with on
  • If you don’t just hook it up
  • If so, see if there are any hooks to execute
  • If so, wrap the hook first and then the ontology
  • If there is no accountability directly assign the value to mount
	setProperyDescriptor(key, proxyXHR) {
		let obj = Object.create(null);
		let _this = this;
		
		obj.set = function(val) {+ // see if the attribute starts with on if it is not directly attached to +if(! key.startsWith('on')) {
+				proxyXHR['__' + key] = val;
+				return; +} + // If so, see if there are any hooks to executeif(_this.xhr [key]) {+ // if there are hooks, then ontology + this.xhr [key] =function(... args) { + (_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args)); +},return; + this._xhr[key] = val; } + obj.get =function() {+return proxyXHR['__'+ key] || this._xhr[key]; +},return obj;
	}
Copy the code

The first step is to determine whether the “on” header is not the same as the original hanging on the instance

Then check to see if the current property is in the hook list and wrap the value (val) with the hook into a function

The hook is executed first and then the value of the hook is executed as long as the person using the hook is not blind

If you don’t have a hook you just assign it

So the set method is ok

The get method is a little bit easier just to get the value

ProxyXHR [‘__’ + key] = val;

It’s also in the get method

Why is there an __ prefix here when it’s really like that

Consider a scenario where you get the responseText property when the interception request comes back

This property is the value returned by the server

It may be at this point that the responseType needs to be handled uniformly

Parse (this.responseText) = json.parse (this.response)

And then we’re going to happily go and get responseText in the successful callback function

It turns out that there was an error in the property or method access to it and I print it out and it’s still a string and it didn’t transfer successfully

And it’s just because responseText is read-only that the writable is false in the tag of this property

So you can use a proxy property to solve this problem

Parse (this.responseText) when this.responseText = JSON

So we’re going to get the responseText from the get method and we don’t have the __responseText property yet so we’re going to go back to the native XHR instance and we’re going to get the value back from the server

And then it gets parsed and it gets assigned

When you copy, you’ll have an extra __responseText property in your own XHR instance whose value is processed

And then the responseText value and the get method is going to get the value of __responseText

This solves the problem of native XHR instance attributes being read-only through a layer of attribute brokering

This. OverwriteAttributes (key, proxyXHR); Comment out

Second debugging

It’s possible to get confused in here don’t worry too much about the drawing just get a glass of water and calm down

This is the complete code so far so you can try it out

class AnyXHR {

  constructor(hooks = {}, execedHooks = {}) {
    this.XHR = window.XMLHttpRequest;

    this.hooks = hooks;
    this.execedHooks = execedHooks;

    this.init();
  }

  init() {
    let _this = this;
    window.XMLHttpRequest = function() { this._xhr = new _this.XHR(); // Mount a reserved native XHR instance on the instance _this.overwrite(this); } } overwrite(proxyXHR) {for (let key in proxyXHR._xhr) {

      if (typeof proxyXHR._xhr[key] === 'function') {
        this.overwriteMethod(key, proxyXHR);
        continue;
      }

      this.overwriteAttributes(key, proxyXHR);

    }
  }

  overwriteMethod(key, proxyXHR) {
    let hooks = this.hooks;
    letexecedHooks = this.execedHooks; proxyXHR[key] = (... Args) => {// Execute the hook if the current method has a corresponding hookif (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
        return; } // execute the corresponding method in the native XHR instance const res = proxyxhr._xhr [key]. Apply (proxyxhr._xhr, args); ExecedHooks [key] && execedHooks[key].call(proxyxhr._xhr, res);returnres; }}setProperyDescriptor(key, proxyXHR) {
    let obj = Object.create(null);
    let _this = this;

    obj.set = function(val) {// See if the attribute starts with on if it is not mounted directlyif(! key.startsWith('on')) {
        proxyXHR['__' + key] = val;
        return; } // If so, see if there are any hooks to executeif(_this.hooks[key]) {// This. XHR [key] =function(... args) { (_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args)); }return; } this._xhr[key] = val; } obj.get =function () {
      return proxyXHR['__' + key] || this._xhr[key];
    }

    returnobj; } overwriteAttributes(key, proxyXHR) { Object.defineProperty(proxyXHR, key, this.setProperyDescriptor(key, proxyXHR)); }}Copy the code

call

new AnyXHR({
  open: function () {
    console.log('open');
  },
  onload: function () {
    console.log('onload');
  },
  onreadystatechange: function() {
    console.log('onreadystatechange'); }}); $.get('/aaa', {
  b: 2,
  c: 3
}).done(function (data) {
  console.log(1);
});

var xhr = new XMLHttpRequest();

xhr.open('GET'.'/abc? a=1&b=2'.true);

xhr.send();

xhr.onreadystatechange = function() {
  console.log(1);
}
Copy the code

Introducing jquery to try to intercept

Look at the console to see the results

There’s a little bit of work that needs to be done to make the class better but the rest is pretty straightforward

The singleton

Because it’s a global interception and there’s only one XMLHttpRequest object at the global level, this should be a singleton

Singletons are a kind of design pattern that’s not so mysterious but there’s only one instance globally

How come new AnyXHR always gets the same instance

Modify the constructor

Constructor (hooks = {}, execedHooks = {}) {// singleton +if (AnyXHR.instance) {
+			return AnyXHR.instance;
+		}
		
		this.XHR = window.XMLHttpRequest;
		
		this.hooks = hooks;
		this.execedHooks = execedHooks;
		this.init();
	
+		AnyXHR.instance = this;
	}
Copy the code

Enter the constructor to check whether AnyXHR has a hanging instance. If so, return the instance directly. If not, create the instance

And then when you’re done creating AnyXHR you can just hang an instance on AnyXHR so that no matter what new gets it’s always the same instance, right

Add another method to get the instance

	getInstance() {
	  return AnyXHR.instance;
	}
Copy the code

Dynamic join hook

All hooks are maintained in two objects that are read every time a method is executed so you can add hooks dynamically as long as the object changes

So add an Add method

	add(key, value, execed = false) {
	  if (execed) {
	    this.execedHooks[key] = value;
	  } else {
	    this.hooks[key] = value;
	  }
	  return this;
	}
Copy the code

The key value parameters correspond to the attribute name or method name and value

Execed indicates whether the native method is executed before it is executed. This parameter is used to distinguish which object to add to

Similarly, removing hooks and emptying hooks is simple

Remove the hook

	rmHook(name, isExeced = false) {
	  let target = (isExeced ? this.execedHooks : this.hooks);
	  delete target[name];
	}
Copy the code

The empty hook

	clearHook() {
		this.hooks = {};
		this.execedHooks = {};
	}
Copy the code

Cancel global listener interception

This is actually a very simple step to take our own rewritten XMLHttpRequest and make it the original one

We kept the original on this.xhr

	unset() {
		window.XMLHttpRequest = this.XHR;
	}
Copy the code

Re-listen interception

Since it is a singleton to restart listening, so as long as the singleton clear new

	reset() {
	  AnyXHR.instance = null;
	  AnyXHR.instance = new AnyXHR(this.hooks, this.execedHooks);
	}
Copy the code

The complete code

Class AnyXHR {/** * constructor * @param {*} hooks * @param {*} execedHooks */ constructor(hooks = {}, ExecedHooks = {}) {// singletonif (AnyXHR.instance) {
      returnAnyXHR.instance; } this.XHR = window.XMLHttpRequest; this.hooks = hooks; this.execedHooks = execedHooks; this.init(); AnyXHR.instance = this; } /** * initializes the overwrite XHR object */init() {
    let _this = this;

    window.XMLHttpRequest = function() { this._xhr = new _this.XHR(); _this.overwrite(this); } /** * add(key, value, execed =)false) {
    if (execed) {
      this.execedHooks[key] = value;
    } else {
      this.hooks[key] = value;
    }
    returnthis; } @param {*} XHR */ overwrite(proxyXHR) {for (let key in proxyXHR._xhr) {
      
      if (typeof proxyXHR._xhr[key] === 'function') {
        this.overwriteMethod(key, proxyXHR);
        continue; } this.overwriteAttributes(key, proxyXHR); }} /** * overwriteMethod(key, proxyXHR) {let hooks = this.hooks;
    letexecedHooks = this.execedHooks; proxyXHR[key] = (... Args) => {// interceptif (hooks[key] && (hooks[key].call(proxyXHR, args) === false)) {
        return; } // Implement method body const res = proxyxhr._xhr [key]. Apply (proxyxhr._xhr, args); ExecedHooks [key] && execedHooks[key]. Call (proxyxhr._xhr, res);returnres; }; } /** * overwriteAttributes * @param {*} key */ overwriteAttributes(key, proxyXHR) {object.defineproperty (proxyXHR, key, this.setProperyDescriptor(key, proxyXHR)); } /** * Set the attribute description of the property * @param {*} key */setProperyDescriptor(key, proxyXHR) {
    let obj = Object.create(null);
    let _this = this;

    obj.set = function(val) {// If it is not an on attributeif(! key.startsWith('on')) {
        proxyXHR['__' + key] = val;
        return;
      }

      if (_this.hooks[key]) {

        this._xhr[key] = function(... args) { (_this.hooks[key].call(proxyXHR), val.apply(proxyXHR, args)); }return;
      }

      this._xhr[key] = val;
    }

    obj.get = function() {
      return proxyXHR['__' + key] || this._xhr[key];
    }

    returnobj; } /** * get instance */getInstance() {
    returnAnyXHR.instance; } /** * delete hook * @param {*} name */ rmHook(name, isExeced =false) {
    lettarget = (isExeced ? this.execedHooks : this.hooks); delete target[name]; } /** * empty the hook */clearHook() { this.hooks = {}; this.execedHooks = {}; } /** * Cancel listening */unset() { window.XMLHttpRequest = this.XHR; } /** * re-listen */reset() { AnyXHR.instance = null; AnyXHR.instance = new AnyXHR(this.hooks, this.execedHooks); }}Copy the code

complete

At this point, the whole thing is complete and there may be bugs due to lack of testing

The other drawback is that all hooks have to be synchronous and if they are asynchronous the order will be out of order and we’ll deal with that later if you’re interested you can try it yourself

In addition, this interception method is basically applicable to any object and can be used flexibly

The source code

You can use it to intercept any Ajax request using XMLHttpRequest