📜 ⬆️ ⬇️

Loading javascript files. We solve the problem Ctrl-F5

We all know a hundred ways to load scripts. Each has its pros and cons.

I want to introduce you to the next method of downloading js-files. I also understand that this method is actively used in the network, but I have not seen articles about it.
Therefore, I will describe the way that I use myself, in the hope that you will also like it.

Goals: development modularity, fast loading, valid cache .
Bonus: download indicator
')
UPD. Marked the main purpose of this method - valid cache.
When using this method, you will not have uncertainty about whether the script will be updated and whether it will work for the end user.

UPD 2. For those who do not finish reading to the end (I understand you perfectly), in the end it says how everything can be done much easier.
Instead of core.633675510761.js write core.js? V = 633675510761. And there it is stated why so much has been written.

UPD 3. In the comments from david_mz , WebByte there was a suggestion to use the urlrewrite instead of JSHandler to process the request.



By modularity, I mean that each component of the system is located in a separate file: core.js, utils.js, control.js, button.js, etc.
From this principle, I do not refuse even when loading the page. Although I know that downloading 1 file is 100Kb faster than 10 by 10Kb.
I will solve this problem through caching further.

The speed of loading is all sorts of tricks to display the page as quickly as possible.
The method of combining scripts in the packages I brushed off above. I think its main disadvantages are:

Therefore, minimization, compression and caching remain.
I also refused to compress, because There are opinions that the gain in the speed of downloading a file is lost in the speed of its unpacking.
Caching Here comes the highlight of my way .

In addition to using the “If-Modified-Since” and “If-None-Match” (ETag) headers, I install Expires in a year!
Now why am I so boldly doing that, and I am confident that my file will be valid year.

Because I attribute to the file name the date of its last modification!
Those. there is core.js, inclusion happens like this


Everything, the next change in this file will change its name to core.635675530761.js, and a completely new script will be loaded.
Now I will list the advantages of this method, they are not immediately obvious:


For clarity, here are a couple of screenshots. The Internet is very slow.
The first access to the page.

Second:

We change one file - the third appeal


As you can see from the second and third drawings, the browser has updated the modified script. You can also see that he did not bother to check all files for changes. Those. There are a lot of pictures on the page, but for some reason he checked only two. The same thing happens with scripts. They are not always updated. The web server can set additional headers for static files, like Set-Expires + (1-9999) minutes. Plus, the internal logic of the browser and proxy servers. In general, what we can not affect.

It was a theory. It is not difficult to implement in practice.
I will give an example of how I solve this on ASP.NET. Step by step

1. To include files on the page, I use a special object that checks for the uniqueness of the included file. And then when rending it writes my files with the date prefix.

public class ScriptHelper
{
protected StringCollection includeScripts = new StringCollection();

public void Include( String filename )
{
filename = filename.ToLower();
StringCollection container;
switch ( System.IO.Path.GetExtension( filename ) )
{
case ".js" : container = includeScripts; break ;
default : throw new ArgumentException( "Not supported include file: " + filename, "filename" );
}
if ( !container.Contains( filename ) ) container.Add( filename );
}

public void RegisterScripts( Page page )
{
StringBuilder clientScript = new StringBuilder ();
foreach ( String filename in includeScripts )
clientScript.AppendFormat( includeJS, prefix + FileSystemWatcherManager.GetModifiedName( "Scripts/" + filename ) );
page.ClientScript.RegisterClientScriptBlock( page.GetType(), "clientscripts" , clientScript.ToString(), false );
}
}


* This source code was highlighted with Source Code Highlighter .


Comment:
prefix - relative folder prefix with scripts
FileSystemWatcherManager is a manager for working with physical files. This class allows you to avoid frequent calls to System.IO.File.GetLastWriteTimeUtc (), and is a simple file system monitor wrapper. Let me give you the full code.

using System;
using System.IO;
using System.Collections. Generic ;

public class FileSystemWatcherManager
{
private static String physicalAppPath;
private static SortedList< String , Int64 > lastModifiedFiles = new SortedList< String , Int64 >();

public static void StartDirectoryWatcher( String directory, String filter )
{
#if DEBUG
return ;
#endif
if ( physicalAppPath == null && System.Web. HttpContext .Current.Request != null )
physicalAppPath = System.Web. HttpContext .Current.Request.PhysicalApplicationPath;

foreach ( String pattern in filter.Split( ',' ) )
{
FileSystemWatcher dirWatcher = new FileSystemWatcher( directory, pattern );
dirWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
dirWatcher.IncludeSubdirectories = true ;
dirWatcher.EnableRaisingEvents = true ;
dirWatcher.Changed += new FileSystemEventHandler( OnFileSystemChanged );
dirWatcher.Created += new FileSystemEventHandler( OnFileSystemChanged );
dirWatcher.Renamed += new RenamedEventHandler( OnFileSystemRenamed );

UpdateLastModifiedFiles( directory, pattern, true );
}
}

private static void OnFileSystemRenamed( object sender, RenamedEventArgs e )
{
UpdateLastModifiedFiles( Path.GetDirectoryName( e.FullPath ), ( (FileSystemWatcher)sender ).Filter, true );
}

private static void OnFileSystemChanged( object sender, FileSystemEventArgs e )
{
UpdateLastModifiedFiles( Path.GetDirectoryName( e.FullPath ), ((FileSystemWatcher)sender).Filter, true );
}

public static void UpdateLastModifiedFiles( String directory, String filter, Boolean logAction )
{
lock ( lastModifiedFiles )
{
if ( logAction ) WL.Logger.Instance.Log( String .Format( "Update modified files {1} at \"{0}\"" , directory, filter ) );

foreach ( String subDir in Directory .GetDirectories( directory ) )
UpdateLastModifiedFiles( subDir, filter, false );
foreach ( String file in Directory .GetFiles( directory, filter ) )
lastModifiedFiles[file.Substring( physicalAppPath.Length ).ToLower().Replace( '\\' , '/' )] = File .GetLastWriteTimeUtc( file ).Ticks / 1000000;
}
}

public static String GetModifiedName( String clientPath )
{
#if DEBUG
return clientPath;
#endif
lock ( lastModifiedFiles )
{
Int64 ticks;
if ( !lastModifiedFiles.TryGetValue( clientPath.ToLower(), out ticks ) ) return clientPath;
return String .Format( "{0}/{1}.{2}{3}" , Path.GetDirectoryName( clientPath ).Replace( '\\' , '/' ), Path.GetFileNameWithoutExtension( clientPath ), ticks, Path.GetExtension( clientPath ) );
}
}
}


* This source code was highlighted with Source Code Highlighter .


Call global.asax

void Application_Start( object sender, EventArgs e )
{
FileSystemWatcherManager.StartDirectoryWatcher( HttpContext .Current.Request.PhysicalApplicationPath, "*.js,*.css" );
}


* This source code was highlighted with Source Code Highlighter .


I think comments are superfluous, the only thing I will note is that under DEBUG mode, I use real file names so that the debugger can cling to them.

The next item is a js file handler.
Enabled via web.config
<httpHandlers>
<add verb="GET" path="*.js" type="WL.JSHandler"/>
</httpHandlers>


The handler is needed to remove the prefix with the time of the change and give the real file. It also checks the If-None-Match headers, If-Modified-Since, sets LastModified, ETag and Expires. It is also possible to select a file, original, minimized, compressed, checking rights and so on.

I give the clothed version.
public class JSHandler : IHttpHandler
{
public void ProcessRequest( HttpContext context )
{
try
{
String filepath = context.Request.PhysicalPath;
String [] parts = filepath.Split( '.' );
Int64 modifiedTicks = 0;
if ( parts.Length >= 2 )
{
if ( Int64 .TryParse( parts[parts.Length - 2], out modifiedTicks ) )
{
List < String > parts2 = new List < String >( parts );
parts2.RemoveAt( parts2.Count - 2 );
filepath = String .Join( "." , parts2.ToArray() );
}
}

FileInfo fileInfo = new FileInfo( filepath );
if ( !fileInfo.Exists )
{
context.Response.StatusCode = 404;
context.Response.StatusDescription = "Not found" ;
}
else
{
DateTime lastModTime = new DateTime ( fileInfo.LastWriteTime.Year, fileInfo.LastWriteTime.Month, fileInfo.LastWriteTime.Day, fileInfo.LastWriteTime.Hour, fileInfo.LastWriteTime.Minute, fileInfo.LastWriteTime.Second, 0 ).ToUniversalTime();
String ETag = String .Format( "\"{0}\"" , lastModTime.ToFileTime().ToString( "X8" , System.Globalization.CultureInfo.InvariantCulture ) );
if ( ETag == context.Request.Headers[ "If-None-Match" ] )
{
context.Response.StatusCode = 304;
context.Response.StatusDescription = "Not Modified" ;
}
else
if ( context.Request.Headers[ "If-Modified-Since" ] != null )
{
String modifiedSince = context.Request.Headers[ "If-Modified-Since" ];
Int32 sepIndex = modifiedSince.IndexOf( ';' );
if ( sepIndex > 0 ) modifiedSince = modifiedSince.Substring( 0, sepIndex );
DateTime sinceDate;
if ( DateTime .TryParseExact( modifiedSince, "R" , null , System.Globalization.DateTimeStyles.AssumeUniversal, out sinceDate ) &&
lastModTime.CompareTo( sinceDate.ToUniversalTime() ) == 0 )
{
context.Response.StatusCode = 304;
context.Response.StatusDescription = "Not Modified" ;
}
}
if ( context.Response.StatusCode != 304 )
{
String file = fileInfo.FullName;

/* String encoding = context.Request.Headers["Accept-Encoding"];
if( encoding != null && encoding.IndexOf( "gzip", StringComparison.InvariantCultureIgnoreCase ) >= 0 &&
File.Exists( file + ".jsgz" ) )
{
file = file + ".jsgz";
context.Response.AppendHeader( "Content-Encoding", "gzip" );
}
else*/
if ( File .Exists( file + ".jsmin" ) ) file = file + ".jsmin" ;

if ( context.Request.HttpMethod == "GET" )
{
context.Response.TransmitFile( file );
}

context.Response.Cache.SetCacheability( HttpCacheability.Public );
context.Response.Cache.SetLastModified( lastModTime );
context.Response.Cache.SetETag( ETag );
if ( modifiedTicks != 0 )
context.Response.Cache.SetExpires( DateTime .UtcNow.AddYears( 1 ) );
context.Response.AppendHeader( "Content-Type" , "text/javascript" );

context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK" ;
}
}
}
catch ( Exception ex )
{
WL.Logger.Instance.Error( ex );
context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error" ;
}
}

public bool IsReusable { get { return true ; } }
}


* This source code was highlighted with Source Code Highlighter .


Remark If I noticed, I give away either the original file, or .jsmin, or .jsgz.
These are minimized and compressed versions, which are built automatically by a separate tool when building a server. To prevent direct access to them, add them to web.config


<httpHandlers>
<add verb="*" path=".jsmin" type="System.Web.HttpForbiddenHandler"/>
<add verb="*" path=".jsgz" type="System.Web.HttpForbiddenHandler"/>
</httpHandlers>


Instead of ending


Perhaps you will say a lot of difficulties with the implementation of a separate handler.
I can advise a simpler way to handle the file. Instead of writing
Then do not need JSHandler. But I'm not sure how the cache will work. In fact, the file name does not change, but only an additional parameter appears. Those. The same problem with Ctrl-F5 may occur due to the internal browser cache or proxy server.
But I still need a separate JSHandler to check the rights to access scripts, for example, from the Admin folder I give only to admins.
Plus, it will be useful and interesting for someone to see the implementations of JSHander and the file system monitor.

If the JSHandler task is only in cutting the modification key, then it can be replaced by the urlrewrite module.

Obviously, in this way you can load other types of files, for example .css. I do that, it can be seen on the first screenshot, I deliberately pointed the cursor on the css-file.
You can expand to other types, such as images. But it is inappropriate. First of all, you are tortured in the code to put down the right names, and secondly, the pictures change extremely rarely, so if one gets stuck in the browser cache, then it’s not scary. And if it's scary, rename this picture manually.

And the promised bonus with the download indicator.


While your scripts are being loaded for the first time, you can visually speed up the process by showing the download process.
Remember the place where I write include files? There actually is a code like this:

StringBuilder clientScript = new StringBuilder ();
if ( includeScripts.Count > 0 )
{
clientScript.Append( @"<div id=" "preloader" " style=" "display:none" "><div></div>Loading Scripts...</div>" );
}
clientScript.Append( scriptStart );
if ( includeScripts.Count > 0 )
{
clientScript.Append( @"var pl=document.getElementById(" "preloader" ");pl.style.display=" "" ";pl=pl.firstChild;" );
}
clientScript.Append( scriptEnd );
if ( includeScripts.Count > 0 )
{
Single dx = 100f / includeScripts.Count;
Single pos = 0f;
foreach ( String filename in includeScripts )
{
clientScript.AppendFormat( includeJS, prefix + FileSystemWatcherManager.GetModifiedName( "Scripts/" + filename ) );
clientScript.AppendFormat( @"<script type=" "text/javascript" ">pl.style.width=" "{0}%" ";</script>" , ( Int32 )pos );
pos += dx;
}
}


* This source code was highlighted with Source Code Highlighter .


Draw two div'a. And the second as the load increases the width.
In CSS, it looks like this:
#preloader { width:218px;height:92px;background:transparent url("../images/preload.jpg") no-repeat scroll left top;position:relative;text-align:right;color:#383922;font-weight:bold;margin-left:20px;margin-right:auto; }
#preloader div { width:0px;height:92px;background:transparent url("../images/preload.jpg") no-repeat scroll left bottom;position:absolute;left:0px;top:0px; }


Thanks for attention. Ready to listen to criticism, comments and answer questions

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


All Articles