Monday, April 20, 2009

CRM Form Script Loader

I wrote this post as a response to a question on ms forums about using XMLHTTP as a mechanism for loading remote scripts.

It seems that using this technique causes a caching problem which can really slow things down while developing.

Here is the notorious script:


function load_script (url)
{
var x = new ActiveXObject("Msxml2.XMLHTTP");
x.open('GET', url, false); x.send('');
eval(x.responseText);

var s = x.responseText.split(/\n/);

var r = /^function\s*([a-z_]+)/i;
for (var i = 0; i < s.length; i++)
{
var m = r.exec(s[i]);
if (m != null)
{
window[m[1]] = eval(m[1]);
}
}
}

load_script("/_customscript/customscript1.js");
load_script("/_customscript/customscript2.js");
load_script("/_customscript/customscript3.js");


In my opinion loading scripts using XMLHTTP should be avoided! Why?
1. The eval() function is very slow and should only be used as a last resort i.e. if no other options are available.
2. RegExp is also known to be slow especially with large documents.
3. Using XMLHTTP synchronously hangs / blocks the entire page until each resource is fully loaded.


Here is a better choice. Why?
1. Caching can be avoided easily.
2. Works much faster.
3. Does not block the page natural loading order and does not wait for other resources to finish loading.


function ScriptLoader(func)
{
var loader = this;

/* if ScriptLoading > 0 then scripts are still loading */
loader.ScriptLoading = 0;
/* the script entity point e.g. OnCrmPageLoad */
loader.Init = func;
/*
url - script url
noc - include a nocache querystring parameter
*/
loader.Load = function(url,noc)
{
var script = document.createElement("SCRIPT");
script.src = url + (noc ? "?nocache=" + Math.random() : "");

script.onreadystatechange = function()
{
if (!(script.readyState == 'loaded' || script.readyState == 'complete'))
{
return;
}

/* if loader.ScriptLoading > 0 true else false */
if (--loader.ScriptLoading)
{
return;
}

/* finished loading (loader.ScriptLoading == 0) , call entry point */
loader.Init();
}

/* append the script to the head tag */
document.documentElement.childNodes[0].appendChild(script);
/* indicate that this script is loading */
loader.ScriptLoading++;
}
}

/*
create a Script loader object + function to be called when the script has
finished loading
*/
window.ScriptLibrary = new ScriptLoader(OnCrmPageLoad);

/* url , false - cache, true - no cache */
ScriptLibrary.Load('/ISV/SCLIB/JScript1.js',false);
ScriptLibrary.Load('/ISV/SCLIB/JScript2.js',true);
ScriptLibrary.Load('/ISV/SCLIB/JScript3.js',false);

/* Start Script Execution */
function OnCrmPageLoad()
{
var res = JS2Function();
res += "," + JS1Function();
res += "," + JS3Function();
alert(res)
}


The remote scripts functions can be written in such a way (e.g. window.FuncName = function(){ /* code */ }) which immediately exposes them to the window scope e.g.


// JScript3.js File
window.JS3Function = function()
{
return 'JS3Function';
}

// JScript2.js File
window.JS2Function = function()
{
return 'JS2Function';
}

// JScript1.js File
window.JS1Function = function()
{
return 'JS1Function';
}

2 comments:

Steve Le Mon said...

Hi Adi

The 'notorious' script code for me is easier to understand. All 22 lines would be placed within the Form Load event of a form and any functions or business code would go in the remote script files via Visual Studio.

Now, I fully accept you argument that this isn't the best way to do it, so I'm happy to remove this code in faviour of your alternative, however I'm a little lost as to what goes where.

I don't understand how the function OnCrmPageLoad() routine works (with the alert box), or the remote script function examples.

Does this mean that only a single function can exist in each file and what about remote scripting of global variable.

If I had code to hide a section how would I record it in my remote files and how would it be called.

Regards

Steve

Anonymous said...

it's not good idea to use your code in OnLoad event.