📜 ⬆️ ⬇️

URL.js or friends with javascript handling links

image

Good day, dear habravchane!

Today I faced the task of generating GET parameters and the entire URL as a whole, on the client side, right here right now, without the ability to "talk" with the server. Immediately make a reservation, I learned about this post right before writing this article because I first finished writing and then resorted to searching, and that post is not with everything about the same thing that I have.
')
So, to the point.

Challenge and problems


The problems are the same as in the post that I mentioned above:

Tasks that I set for myself:


I wrote in the cleanest JavaScript, and without using prototype.__defineGetter__ or prototype.__defineSetter__ in favor of cross-browser compatibility, because IE <9 cannot do this. More details about getters / setters are written in this post.

For those who are interested, we will sort it out, and who needs a ready-made solution - you are welcome to the end of the post, the download links are there.

Let's get started! Before we sit down - we will leave earlier.


Constructor


Constructor code
 var URL = function( param, param2 ){ param = param || false; param2 = ( param2 === false ) ? false : true; this.urlEncode = param2; this.data = { scheme: false, user: false, pass: false, host: false, port: false, path: false, query: false, params: {}, fragment: false }; if( typeof(param) == 'string' ){ this.url = param; this.parse(); } else if ( typeof(param) == 'object' ){ for(var key in param){ if( this.data.hasOwnProperty( key ) ){ if( param[ key ] || ( key == 'params' && typeof(param.params) == 'object' ) ) this.data[ key ] = param[ key ]; } } this.update(); } } 


Read more



Parsing


It is necessary to parse the existing URL, we will do this with the help of RegExp . No, you can of course handle everything with str.split() , but this, I think, is a special kind of fetishism.
 regExp = /^(?:([a-z0-9_\-\.]+):\/\/)*(?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)*([a-z0-9][a-z0-9_\-\.]+)(?:\:([\d]+))*(?:\/([^?#]*))*(?:\?([^?#]*))*(?:\#([^?#]*))*/gi; 

And in parts
  • (?:([a-z0-9_\-\.]+):\/\/)* - SCHEME, if you believe Wikipedia , the scheme has the form :// and, there may be - and _ . For the sake of universality, it is installed * i. scheme may not be specified.
  • (?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)* - USER: PASSWORD, password without username does not happen, and username without a password happens.
  • ([a-z0-9][a-z0-9_\-\.]+) - HOST, as far as I know, a domain name can begin only with a letter / number, and then both - and _ and can go. Moreover, there are no domain names shorter than 6 characters, but after all, links are also intranet, where you want to drive as hostnames, the one that comes down and 1+ symbol.
  • (?:\:([\d]+))* - PORT, this parameter is optional,: and then digits
  • (?:\/([^?#]*))* - PATH, the path to the file, in general, in theory, is any number of any characters, but we cut it? and # in order not to parse GET parameters or a fragmentary pointer into the path. The path may be unspecified.
  • (?:\?([^?#]*))* - QUERY, set of GET parameters, key = value pairs. It may also not be specified.
  • (?:\#([^?#]*))* - FRAGMENT, fragmentary pointer. If anyone does not know, then /index.html#fragment tells the browser to scroll to a DOM element with id="fragment"


Of course, it will work in all languages ​​that understand RegExp. Use, do not hesitate.

Parser
  parse: function(){ this.res = /^(?:([a-z0-9_\-\.]+):\/\/)*(?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)*([a-z0-9][a-z0-9_\-\.]+)(?:\:([\d]+))*(?:\/([^?#]*))*(?:\?([^?#]*))*(?:\#([^?#]*))*/gi.exec( this.url ); this.data.scheme = this.res[ 1 ] || false; this.data.user = this.res[ 2 ] || false; this.data.pass = this.res[ 3 ] || false; this.data.host = this.res[ 4 ] || false; this.data.port = this.res[ 5 ] || false; this.data.path = this.res[ 6 ] || false; this.data.query = this.res[ 7 ] || false; this.data.fragment = this.res[ 8 ] || false; if( this.data.query ){ this.parts = this.data.query.split( '&' ); for( var i = 0; i < this.parts.length; i++ ){ param = this.parts[ i ].split( '=' ); this.data.params[ param[ 0 ] ] = decodeURIComponent( param[ 1 ] ); } } delete this.res; delete this.parts; } 


Nothing regExp : splitting the regExp above and saving the data to this.data hash
Unless, I mentioned earlier - convenient work with GET-parameters of the url is necessary, and therefore we split the query with the help of split (split () in this case “cheaper” than regExp ) and save it to the same notorious hash. It is worth noting the use of decodeURIComponent, because GET-parameters can be urlencoded.

Option 1. "Beauty"


Getters / setters


For convenient work with reading / changing parameters, I decided to choose JS way getters and setters. Te method by property name and if the method is called with the parameter specified, this is setter, if without a parameter it is getter.
I will URL.prototype = { } them through the URL.prototype = { } so as not to produce redundant instances of the method in memory.
I will give one method as an example, because they are similar:
  scheme: function( param ){ if( typeof( param ) != 'undefined' ){ this.data.scheme = param; return this.update(); } else { return this.data.scheme ? this.data.scheme : false; } } 

I note that in the case of a change in the value, it is not String that is returned, but Object done so that you can write the setter chains:
 var url = new URL(); url.scheme('https').host('example.com').path('index.php').params({'p1':"v1", 'p2':"2"}).url; // : https://example.com/index.php?p1=v1&p2=%D0%B22 


Separately dwell on the getter / setter for the params property
  params: function( param1, param2 ){ if( typeof( param1 ) != 'undefined' ){ if( typeof( param1 ) == 'string' ){ if( typeof( param2 ) != 'undefined' && ( param2 == '' || param2 === false ) ){ if( this.data.params.hasOwnProperty( param1 ) ){ delete this.data.params[ param1 ]; } } else if( typeof( param2 ) != 'undefined' ){ this.data.params[ param1 ] = param2; } else{ return this.data.params[ param1 ] ? this.data.params[ param1 ] : false; } } else if( typeof( param1 ) == 'object' ){ for( var key in param1 ){ if( typeof( param1[ key ] ) != 'undefined' && ( param1[ key ] == '' || param1[ key ] === false ) ){ if( this.data.params.hasOwnProperty( key ) ) delete this.data.params[ key ]; } else{ this.data.params[ key ] = param1[ key ]; } } } return this.update(); } else { return this.data.params ? this.data.params : false; } } 

As you can see, both parameters are optional.
And as I said - I set myself the goal - the convenience of working with GET-parameters, which means we should be able to:

both a single parameter and a group of parameters.

Accordingly, the syntax will be as follows:


Putting the URL back


As you noticed, this.update() calls 2 functions in getters:

Code collector
  update: function(){ this.data.query = ''; for( var key in this.data.params ){ this.data.query += this.urlEncode ? key+'='+encodeURIComponent( this.data.params[ key ] )+'&' : key+'='+this.data.params[ key ]+'&'; } if( this.data.query ) this.data.query = this.data.query.slice( 0, -1 ); this.url = ''; this.url += this.data.scheme ? this.data.scheme+'://' : ''; this.url += this.data.user ? this.data.user+':' : ''; this.url += this.data.pass ? this.data.pass+'@' : ''; this.url += this.data.host ? this.data.host+'/' : ''; this.url += this.data.path ? this.data.path : ''; this.url += this.data.query ? '?'+this.data.query : ''; this.url += this.data.fragment ? '#'+this.data.fragment : ''; return this; } 


It is worth noting that when assembling GET parameters, the values ​​of the parameters are converted to escape sequence.
First: it is right.
Secondly: if we receive data entered by the user with the GET-parameter, then the ampersand inserted by the user will destroy the key-value sequence and everything will roll into the map.

Well, if so, straight from the nose, you do not need a urlencoded line - you have two options:
Pass in the second parameter in the constructor false
  1. Manually set the URL.urlEncode=false; property. URL.urlEncode=false;
  2. Call the URL.update(); method URL.update();

 test = new URL({"path":"index.php", "params":{"param1":"value1", "param2":" &"}}, false); test.url; //index.php?param1=value1¶m2= & test2 = new URL({"path":"index.php", "params":{"param1":"value1", "param2":" &"}}); test2.url; //index.php?param1=value1¶m2=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%B0%26 test2.urlEncode=false; test2.update().url; //index.php?param1=value1¶m2= & 


Well, and so it was convenient - a method for clicking on the generated link:
  go: function(){ if(!this.data.scheme && this.data.host) this.data.scheme = 'http'; window.location.href = this.update().url; } 

As you can see: if the scheme is not specified, but the host is specified, the http scheme is automatically substituted as the most common.
Next, the link is updated and followed.

Extend the String object


In theory, this could be finished. But, it seemed to me that it would be convenient to work directly with string variables without explicitly creating an object instance (no matter how strange it sounds, but, in JS, there are no classes as such).

As usual I will give an example of one method:
 String.prototype.scheme = function( param ){ var url = new URL( this.valueOf() ); if( typeof( param ) != 'undefined' ){ url.scheme( param ); result = url.url; } else{ result = url.scheme(); } delete url; return result; } 

In general, the code simply passes the parameters to the appropriate method of the URL object.
But it may seem strange to some that I create and delete every call to URL objects and do only one action, and this action does not change the value of the variable it is being performed on.
This is where the most important inconvenience of the String object lies, it is impossible to change the value of an existing variable. With it nothing can be done at all, ALWAYS creates a new variable. That is why every time a new object is created and a variable of the String type is returned.
Chains are of course supported:
 url = 'example.com'; url.scheme('https').path('index.php').params({'p1':"v1", 'p2':"2"}); // : https://example.com/index.php?p1=v1&p2=%D0%B22 


Option 2. "By Feng Shui"


If the previous version, let's say, was “beautiful” to use, then this option would be concise. both in terms of code and in terms of use.

Getters / setters


So, getter / setter in this case will be one for everything, well, that is, quite.
  val: function( key, param, param2 ){ if( this.data.hasOwnProperty( key ) ){ if( typeof( param ) == 'undefined' ){ return this.data[ key ] ? this.data[ key ] : false; } else if( typeof( param ) != 'undefined' ){ if( key == 'params' ){ if( typeof( param ) == 'string' ){ if( typeof( param2 ) != 'undefined' ){ this.data[ key ][ param ] = param2; } else{ return this.data[ key ][ param ] ? this.data[ key ][ param ] : false; } } else if( typeof( param ) == 'object' ){ for( var keys in param ){ if( typeof( param[ keys ] ) != 'undefined' && ( param[ keys ] == '' || param[ keys ] === false ) ){ if( this.data[ key ].hasOwnProperty( keys ) ){ delete this.data[ key ][ keys ]; } } else{ this.data[ key ][ keys ] = param[ keys ]; } } } } else{ this.data[ key ] = param; } return this.update(); } } else return 'undefined'; } 


Extend the String object


The situation is identical with the extension of the String object, only the code is smaller, since this method only transports parameters to URL.val ();

Summarizing


So, at the exit, we have a lib, which gives us the opportunity to work adequately with the URL, and not only to parse, but also to change individual fate of the URL. This is not to mention a very convenient, in my opinion, tool for working with GET-parameters.

Pros and cons of approaches

Option 1

Pros:

Minuses:


Option 2

Pros:

Minuses:


Download the sources of both options here: [ Option 1 || Option 2 ]. I do not see the point of putting it on the githab, for there is only 1 file.
Support:


And for this - I will take my leave, sincerely I hope that my post will be of benefit to someone.
All good code, more sleep and that IE does not spoil life.

Source: https://habr.com/ru/post/232073/


All Articles