Add a Version Parameter to the ScriptSrc URL in a ScriptLink CustomAction

The CustomAction element provides a great way to inject a JavaScript file into every page in a site collection or site without having to explicitly reference the file in a master page. You simply set Location to “ScriptLink”, specify the path to the JavaScript file in the ScriptSrc attribute and it will generate a script tag. This is particularly handy in situations where you may need script to execute in pages that don’t use your master page like the PerformancePoint application pages that open in a new window.

Reload in Netscape

“You need to click the Reload button. And upgrade your browser.”

One problem you can run into with any JavaScript external script include is that users may be seeing stale versions of a script file. Whenever you update a script, you may find that need to have your users explicitly refresh the page to get the latest changes to the file. If they don’t refresh, they will likely be getting a locally-cached version of the file.

If you use a ScriptLink control to add script tags to your page, it takes care of this problem for you. The ScriptLink control will automatically append a “rev” query string parameter to the URL of the referenced script file that contains a version ID for that file so when the script is updated on the server, the new query string parameter forces the browser to request the latest version of the file. The browser can then continue to serve this file from cache until the version on the server is updated again.

Behind the scenes, the ScriptLink control calls the SPUtility.MakeBrowserCacheSafeLayoutsUrl method to find the script file in the layouts directory, calculate a hash on that file and return a modified URL that includes the hash value in the rev parameter.

It would be nice if scripts included through a CustomAction element were treated the same way. Unfortunately, this is not the case. Let’s say you have an Elements.xml like the one below.

<? xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="CustomScript"
Location="ScriptLink"
ScriptSrc="/_layouts/example/customscript.js"
Sequence="1000"
/>
</Elements>

SharePoint will render a JavaScript document.write() call to create a script tag in the document head just after some basic SharePoint scripts including init.js. While init.js and other SharePoint scripts have a rev parameter, the custom script does not.

document.write('<script type="text/javascript" 
    src="/_layouts/1033/init.js?rev=BjQJs0OCQh3Zfydsdw2MYw%3D%3D"></' 
    + 'script>');
//...other scripts...
document.write('<script type="text/javascript" 
    src="/_layouts/example/customscript.js"></' 
    + 'script>');

However, we do know at build and package time that our script may have been updated and we can add our own query string parameter to the URL in the ScriptSrc attribute. This need not be a manual process. The Powershell script below mimics the way the hash is calculated in MakeBrowserCacheSafeLayoutsUrl and appends a rev parameter to the query string in the ScriptSrc attribute in the Elements.xml file.

Write-Host "Starting Custom Powershell Pre-build script"
$ErrorActionPreference = "Stop"

function UpdateScriptSrcWithFileHash()
{
  Param(
    [string]$projPath,
    [string]$elementsFile,
    [string]$elementID,
    [string]$jsFile)

  # Update elements.xml to append query string rev parameter to URL of .js file in
  # ScriptSrc attribute. The parameter is based on the hash of the .js file and 
  # ensures that a new version of the script is requested by a browser every time 
  # the script changes.

  $elementsPath =  Join-Path $projPath $elementsFile

  # read elements.xml and get scriptSrc
  [xml]$elementsXml = Get-Content $elementsPath

  $nm = New-Object Xml.XmlNamespaceManager($elementsXml.NameTable)
  $nm.AddNamespace('sp', 'http://schemas.microsoft.com/sharepoint/')
  $targetElement = $elementsXml.SelectSingleNode(
    '/sp:Elements/sp:CustomAction[@Id="' + $elementID + '"]', $nm)
  $scriptSrc = $targetElement.ScriptSrc

  # calculate hash for .js file
  $jsPath =  Join-Path $projPath $jsFile
  [IO.FileInfo]$jsFile = $jsPath
  $stream = $jsFile.OpenRead()
  $md5Provider = New-Object Security.Cryptography.MD5CryptoServiceProvider
  $hashBytes = $md5Provider.ComputeHash($stream)
  $stream.Close()
  $hash = [Convert]::ToBase64String($hashBytes)
  $hash = [Uri]::EscapeDataString($hash)

  # update ScriptSrc if hash is different
  $newScriptSrc = ($scriptSrc.Split('?'))[0] + "?rev=$hash"
  if ($scriptSrc -ne $newScriptSrc)
  {
    Write-Host "Updating ScriptSrc"
    $targetElement.ScriptSrc = $newScriptSrc
    $elementsXml.Save($elementsPath)
  }

}

try
{
  $projPath = $args[0]
  UpdateScriptSrcWithFileHash $projPath "Example\Elements.xml" 
    "CustomScript" "\Layouts\example\customscript.js"
  Write-Host "Custom Powershell Pre-build script complete."

}
catch
{
  $error
  Write-Host "Custom Powershell Pre-build script failed."
  [Environment]::Exit(1)
}

To use this in a Visual Studio 2010 project, simply paste the above code into a new file named “prebuild.ps1” in the root of your project and then add the following to the Pre-build event command line under Project, [YourProject] Properties, Build Events.

There are a couple of things to watch out for:

  • The script will fail if it cannot write to the Elements.xml file which would be the case if you don’t have Elements.xml checked out. You can see any error details if you look in the Output window.
  • Whenever you update the Elements.xml file, you must use the Replacement method rather than the Update method to upgrade your solution.
Advertisements

One Response to Add a Version Parameter to the ScriptSrc URL in a ScriptLink CustomAction

  1. Remember to start the paths with / or /_layouts as in the sample above. If not you will get an error when ASP.Net tries to parse the link as it will bomb on the ? character.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: