Saturday, June 18, 2016
Client Side Caching for jQuery
Updates: 6/26/16
- Fixed bug where triple equals null check would miss.
- Added support for data driven cache key.
- Removed let and const statements (some minifiers were having a hard time with them)
Original:
There is great question on Stack Overflow about caching a jquery ajax response in javascript/browser. Unfortunately, even thought it was a good solution, it did not do quite what I needed it to.
The application I was trying to optimize sometimes made redundant parallel requests, and I needed my caching solution to include a queuing system to prevent duplicate fetches.
Below is a simple solution that uses jQuery.ajaxPrefilter to check a local cache prior to making GET requests. Additionally, it will queue the callback if the request is already in flight. The cache stores the queue in both memory and local storage, ensuring that the cache will persist across page loads.
Implementation
(function ($) {
"use strict";
var timeout = 60000;
var cache = {};
$.ajaxPrefilter(onPrefilter);
function onPrefilter(options, originalOptions) {
if (options.cache !== true) {
return;
}
var callback = originalOptions.complete || $.noop;
var cacheKey = getCacheKey(originalOptions);
options.cache = false;
options.beforeSend = onBeforeSend;
options.complete = onComplete;
function onBeforeSend() {
var cachedItem = tryGet(cacheKey);
if (!!cachedItem) {
if (cachedItem.data === null) {
cachedItem.queue.push(callback);
} else {
setTimeout(onCacheHit, 0);
}
return false;
}
cachedItem = createCachedItem();
cachedItem.queue.push(callback);
setCache(cacheKey, cachedItem);
return true;
function onCacheHit() {
invoke(callback, cachedItem);
}
}
function onComplete(data, textStatus) {
var cachedItem = tryGet(cacheKey);
if (!!cachedItem) {
cachedItem.data = data;
cachedItem.status = textStatus;
setCache(cacheKey, cachedItem);
var queuedCallback;
while (!!(queuedCallback = cachedItem.queue.pop())) {
invoke(queuedCallback, cachedItem);
}
return;
}
cachedItem = createCachedItem(data, textStatus);
setCache(cacheKey, cachedItem);
invoke(callback, cachedItem);
}
}
function tryGet(cacheKey) {
var cachedItem = cache[cacheKey];
if (!!cachedItem) {
var diff = new Date().getTime() - cachedItem.created;
if (diff < timeout) {
return cachedItem;
}
}
var item = localStorage.getItem(cacheKey);
if (!!item) {
cachedItem = JSON.parse(item);
var diff = new Date().getTime() - cachedItem.created;
if (diff < timeout) {
return cachedItem;
}
localStorage.removeItem(cacheKey);
}
return null;
}
function setCache(cacheKey, cachedItem) {
cache[cacheKey] = cachedItem;
var clone = createCachedItem(cachedItem.data, cachedItem.status, cachedItem.created);
var json = JSON.stringify(clone);
localStorage.setItem(cacheKey, json);
}
function createCachedItem(data, status, created) {
return {
data: data || null,
status: status,
created: created || new Date().getTime(),
queue: []
};
}
function invoke(callback, cachedItem) {
if ($.isFunction(callback)) {
callback(cachedItem.data, cachedItem.status);
}
}
function getCacheKey(originalOptions) {
if (!!originalOptions.data) {
return originalOptions.url + "?" + JSON.stringify(originalOptions.data);
}
return originalOptions.url;
}
})(jQuery);
Enjoy,
Tom
Saturday, January 31, 2015
Making jQuery a bit more Angular
One of my favorite features of AngularJS is the use of HTML attributes to apply controllers and directives directly to your DOM elements. Why is this so useful?
- It is intuitive for developers to discover what code is being applied to elements.
- It enables generic registration, removing boiler plate document ready methods.
- It provides hierarchical scope, encouraging single responsibility controls.
touch events. While jQuery Mobile does not offer a custom feature build that includes only the touch system, it is actually quite easy to create your own build.
Just download the following files and include them in your project; but be sure to delete the define method wrapper (the first and last line of each file), as you do not need them with the complete jQuery build.
...that's it, you can now use only jQuery Mobiles touch events!
Saturday, August 24, 2013
XUnit.PhantomQ v1.1
I recently blogged about how to Use XUnit to Run QUnit Tests. The initial v1.0 release of XUnit.PhantomQ did not support error messages, but now in v1.1 it supports the must have feature of bringing error messages back with failed test results.
XUnit.PhantomQ on NuGet
XUnit.PhantomQ Source on GitHub
Enjoy,
Tom
Sunday, July 14, 2013
Use XUnit to Run QUnit Tests
I read a great article recently about Unit testing JavaScript in VisualStudio with ReSharper, written by Chris Seroka. As cool as this feature is, it left me with two questions:
- What about developers that do not have ReSharper?
- How do I run my JavaScript unit tests on a builder server?
It is no secret that I, absolutely, love, xUnit! Thus I decided to extend xUnit theories to be able to run QUnit tests by implementing a new DataAttribute.
Introducing XUnit.PhantomQ
XUnit.PhantomQ is a little NuGet package you can install to get access to the QUnitDataAttribute (see below for an example). This library will allow you to execute your QUnit tests as XUnit tests.
XUnit.PhantomQ supports both library and web projects, and features the ability to easily specify test files and their dependencies by real relative path to the root of your project.
XUnit.PhantomQ on NuGet
XUnit.PhantomQ Source on GitHub
QUnitData Attribute
Here is an example of writing some JavaScript, a file of QUnit tests, and then using an xUnit theory and a QUnitData Attribute to execute all of those tests right inside of Visual Studio.
// Contents of Demo.js
function getFive() {
return 5;
}
// Contents of Tests.js
test('Test Five', function() {
var actual = getFive();
equal(actual, 5);
});
test('Test Not Four', function () {
var actual = getFive();
notEqual(actual, 4);
});
// Contents of QUnitTests.cs
public class QUnitTests
{
[Theory, QUnitData("Tests.js", "Demo.js")]
public void ReturnFiveTests(QUnitTest test)
{
test.AssertSuccess();
}
}
Integrating XUnit, PhantomJS, and QUnit
So, how does this thing work under the hood? Below is the complete pipeline, step by step, of whenever the tests are executed:
- The XUnit test runner identifies your theory tests.
- The QUnitDataAttribute is invoked.
-
A static helper locates PhantomJS.exe and the root folder of your project.
- It will automatically walk up the folder structure and try to find PhantomJS.exe in your packages folder. However, you can override this and explicitly set the full path by adding an AppSetting to your config file:
<add key="PhantomQ.PhantomJsExePath" value="C:/PhantomJS.exe" /> - The same goes for the root of your project, you can override this location with another AppSetting to your config:
<add key="PhantomQ.WorkingDirectory" value="C:/Code/DemoProject" />
- It will automatically walk up the folder structure and try to find PhantomJS.exe in your packages folder. However, you can override this and explicitly set the full path by adding an AppSetting to your config file:
- PhantomJS.exe is invoked as a separate process.
- The server loads up XUnit.PhantomQ.Server.js
- The server now opens XUnit.PhantomQ.html
- QUnit is set to autoRun false.
- Event handlers are added to QUnit for testDone and done.
- All of the dependencies are now added to the page as script tags.
- The test file itself is loaded.
- QUnit.start is invoked.
- The server waits for the test to complete.
- Upon completion the server reads the results out of the page.
- The tests and their results are serialized to a JSON dictionary.
- The result dictionary is written to the standard output.
- The resulting JSON string is read in from the process output.
- The results are deserialized using Newtonsoft.JSON
- The results are loaded into QUnitTest objects.
- The QUnitTest array is passed back from the DataAttribute
- Each test run finally calls the AssertSuccess and throws on failure.
...and that's (finally) all folks! If you have any questions, comments, or thoughts on how this could be improved, please let me know!
Shout itEnjoy,
Tom
Saturday, April 13, 2013
Report Unhandled Errors from JavaScript
Logging and aggregating error reports is one of the most important things you can do when building software: 80% of customer issues can be solved by fixing 20% of the top-reported bugs.
Almost all websites at least has some form of error logging on their servers, but what about the client side of those websites? People have a tendency to brush over best practices for client side web development because "it's just some scripts." That, is, WRONG! Your JavaScript is your client application, it is how users experience your website, and as such it needs the proper attention and maintenance as any other rich desktop application.
So then, how do you actually know when your users are experiencing errors in their browser? If you are like the vast majority of websites out there...
You don't know about JavaScript errors, and it's time to fix that!
window.onerror
Browsers do offer a way to get notified of all unhandled exceptions, that is the window.onerror event handler . You can wire a listener up to this global event handler and get back three parameters: the error message, the URL of the file in which the script broke, and the line number where the exception was thrown.
window.onerror = function myErrorHandler(errorMsg, url, lineNumber) {
// TODO: Something with this exception!
// Just let default handler run.
return false;
}
StackTrace.js
JavaScript can throw exceptions like any other language; browser debugging tools often show you a full stack trace for unhandled exceptions, but gathering that information programmatically is a bit more tricky. To learn a bit more about the JavaScript language and how to gather this information yourself, I suggest taking a look at this article by Helen Emerson. However in practice I would strongly suggest you use a more robust tool...
StackTrace.js is a very powerful library that will build a fully detailed stack trace from an exception object. It has a simple API, cross browser support, it handles fringe cases, and is very light weight and unobtrusive to your other JS libraries.
try {
// error producing code
} catch(error) {
// Returns stacktrace from error!
var stackTrace = printStackTrace({e: error});
}
Two Big Problems
- The window.onerror callback does not contain the actual error object.
This is a big problem because without the error object you cannot rebuild the stack trace. The error message is always useful, but file names and line numbers will be completely useless once you have minified your code in production. Currently, the only way you can bring additional information up to the onerror callback is to try catch any exceptions that you can and store the error object in a closure or global variable.
- If you globally try catch event handlers it will be harder to use a debugger.
It would not be ideal to wrap every single piece of code that you write in an individual try catch block, and if you try to wrap your generic event handling methods in try catches then those catch blocks will interrupt your debugger when you are working with code in development.
Currently my suggestion is to go with the latter option, but only deploy those interceptors with your minified or production code.
jQuery Solution
This global error handling implementation for jQuery and ASP.NET MVC is only 91 lines of JavaScript and 62 lines of C#.
Download JavaScriptErrorReporter from GitHub
To get as much information as possible, you need to wire up to three things:
(Again, I suggest that you only include this when your code is minified!)
- window.onerror
- $.fn.ready
- $.event.dispatch
Here is the meat of those wireups:
var lastStackTrace,
reportUrl = null,
prevOnError = window.onerror,
prevReady = $.fn.ready,
prevDispatch = $.event.dispatch;
// Send global methods with our wrappers.
window.onerror = onError;
$.fn.ready = readyHook;
$.event.dispatch = dispatchHook;
function onError(error, url, line) {
var result = false;
try {
// If there was a previous onError handler, fire it.
if (typeof prevOnError == 'function') {
result = prevOnError(error, url, line);
}
// If the report URL is not loaded, load it.
if (reportUrl === null) {
reportUrl = $(document.body).attr('data-report-url') || false;
}
// If there is a rport URL, send the stack trace there.
if (reportUrl !== false) {
var stackTrace = getStackTrace(error, url, line, lastStackTrace);
report(error, stackTrace);
}
} catch (e) {
// Something went wrong, log it.
if (console && console.log) {
console.log(e);
}
} finally {
// Clear the wrapped stack so it does get reused.
lastStackTrace = null;
}
return result;
}
function readyHook(fn) {
// Call the original ready method, but with our wrapped interceptor.
return prevReady.call(this, fnHook);
function fnHook() {
try {
fn.apply(this, arguments);
} catch (e) {
lastStackTrace = printStackTrace({ e: e });
throw e;
}
}
}
function dispatchHook() {
// Call the original dispatch method.
try {
prevDispatch.apply(this, arguments);
} catch (e) {
lastStackTrace = printStackTrace({ e: e });
throw e;
}
}
Identifying Duplicate Errors
One last thing to mention is that when your stack trace arrives on the server it will contain file names and line numbers. The inconsistency of these numbers will make it difficult to identify duplicate errors. I suggest that you "clean" the stack traces by removing this extra information when trying to create a unique error hash.
private static readonly Regex LineCleaner
= new Regex(@"\([^\)]+\)$", RegexOptions.Compiled);
private int GetUniqueHash(string[] stackTrace)
{
var sb = new StringBuilder();
foreach (var stackLine in stackTrace)
{
var cleanLine = LineCleaner
.Replace(stackLine, String.Empty)
.Trim();
if (!String.IsNullOrWhiteSpace(cleanLine))
sb.AppendLine(cleanLine);
}
return sb
.ToString()
.ToLowerInvariant()
.GetHashCode();
}
Integration Steps
This article was meant to be more informational than tutorial; but if you are interested in trying to apply this to your site, here are the steps that you would need to take:
- Download JavaScriptErrorReporter from GitHub.
- Include StackTrace.js as a resource in your website.
-
Include ErrorReporter.js as a resource in your website.
- Again, to prevent it interfering with your JavaScript debugger, I suggest only including this resource when your scripts are being minified.
- Add a report error action to an appropriate controller. (Use the ReportError action on the HomeController as an example.)
- Add a "data-report-url" attribute with the fully qualified path to your report error action to the body tag of your pages.
- Log any errors that your site reports!
Enjoy,
Tom
Saturday, November 17, 2012
jQuery Refresh Page Extension
There are three scenarios when a web page refreshes:
- Honors your cache (does not make resource requests).
- Verifies your cache (causes resources to generate 304s).
- Ignores your cache (causes resources to generate 200s).
For a simple page refresh you want your webpage to generate as few server requests as possible. There any many ways to refresh a page in JavaScript, however they can all have different and sometimes undesirable results. Let's look some code snippets of the aforementioned scenarios in reverse order.
3. Ignore Cache
window.location.reload(true);
This scenario is not usually necessary unless you know that the resources of your page have been updated. Also, using a good minification framework is probably a better way of ensuring that your clients always consume the latest resources with every request.
2. Verify Cache
window.location.reload();
This is obvious similar to the first call, only we are calling an override of the reload method that tells the browser to respect cache. However, this will still cause a series of 304 requests!
1. Honor Cache
window.location.href = window.location.href;
This is obviously a very simple way to refresh the page, and it will honor cache without forcing 304 requests. However, this will not work if your includes a hash tag.
jQuery Plugin
This little jQuery plugin will refresh the page and try to cause as few resource requests as possible.
(function($) {
$.refreshPage = refreshPage;
function refreshPage(reload) {
if (reload === true) {
window.location.reload(true);
return;
}
var i = window.location.href.indexOf('#');
if (i === -1) {
// There is no hash tag, refresh the page.
window.location.href = window.location.href;
} else if (i === window.location.href.length - 1) {
// The hash tag is at the end, just strip it.
window.location.href = window.location.href.substring(i);
} else {
// There is a legit hash tag, reload the page.
window.location.reload(false);
}
}
})(jQuery);
Enjoy,
Tom
Sunday, November 6, 2011
jQuery UI Modal Dialog disables scrolling in Chrome
Is Chrome becoming the new IE?
As much as I love jQuery, I still cannot escape the fact that jQuery UI leaves a lot to be desired. Yesterday I ran across an issue where the jQuery UI modal dialog acted inconsistently in different browsers. Normally opening a modal leaves the background page functionality unaltered, but in Webkit browsers (I ran into this while using Chrome) it disables the page scroll bars.
The Fix
Yes, this bug has already been reported. Yes, it is priority major. No, it won't be fixed anytime soon. For a feature as widely used as the Modal Dialog, I find that kinda sad.
However, thanks to Jesse Beach, there is a tiny little patch to fix this! Here is a slightly updated version of the fix:
(function($) { if ($.ui && $.ui.dialog && $.browser.webkit) { $.ui.dialog.overlay.events = $.map(['focus', 'keydown', 'keypress'], function(event) { return event + '.dialog-overlay'; }).join(' '); } }(jQuery));
Additional Resources
Hope that helps!
Tom
Saturday, September 17, 2011
Object Oriented JavaScript Tutorial
Over the past month I spent a lot of time helping teach a good friend of mine how to write advanced JavaScript. Although he had worked with JavaScript many times before, he did not know a lot of the simple things that make JavaScript the crazy dynamic rich development experience that it is. Together we built up this series of examples to help teach everything from language basics to object orientation.
Why do we need yet another JavaScript tutorial?
JavaScript is an interesting language. It is loosely typed, the object orientation has been hacked in over time, and there are at least 10 ways to do anything. The most important thing you can do when writing JavaScript is to choose a set of conventions and stick with them. Unfortunately with so many different ways to do things out there, it's hard to find tutorials that are consistent in their conventions.
Just like developing in any other programming language, understanding the fundamentals is key to building up an advanced application. This tutorial starts out extremely simple, and incrementally gets more advanced. It shows alternative implementations for different tasks, and tries to explain why I prefer the ones that I do. Meanwhile it always follows a consistent set of conventions.
Let's get to the tutorial!
This tutorial comes in 16 lessons. They are designed to be debugged with Firebug for Firefox. If you don't want to actually run the scripts then you can always just read them by downloading the small htm files below and opening them in notepad (or whichever text editor you prefer).
- JavaScript Types
- Object Properties
- Object Declarations
- Objects vs Arrays
- Object Pointers and Cloning
- Equals Operators
- Closures
- Advanced Closures
- Defining Classes with Closures
- Defining Classes with Prototype
- Function Scope
- Creating Delegates
- Class Inheritance
- Advanced Class Inheritance
- More Advanced Class Inheritance
- Extending jQuery
Additional Resources
If all that wasn't enough or it just got you thirsty for more, here are some additional resources to help start you down the path of becoming a ninja with JavaScript. My recommendation is that whenever you are writing JavaScript you should have w3schools open in a second browser window at all times.
- Downloads
- Learning Resources
- Alternative Tutorials
- JavaScript Frameworks
Enjoy,
Tom
9/21 Update - Thanks to Zach Mayer (of System-Exception.com) for providing a great critique of my tutorial. He pointed out several typos and small bugs in lessons 4, 6, 7, 9, 10, 12, 14, 15.