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
- Keep the native XHR objects
- will
XMLHttpRequest
Object is set to the new object - The methods of the XHR object are overridden and put into the new object
- 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