Skip to content

Instantly share code, notes, and snippets.

@wibblymat
Last active December 12, 2015 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wibblymat/4754410 to your computer and use it in GitHub Desktop.
Save wibblymat/4754410 to your computer and use it in GitHub Desktop.
Discussion of possible plugin architecture for basket.js

#Plugins#

##Motivation##

We have had several requests to add support for different resource types to basket.js. I propose that rather than add new code paths for each resource type directly to the library, we instead provide the features necessary for consumers to provide their own extensions. This discussion covers the options available to us in providing these features along with any associated risks.

##Extending to other text-based resources##

The current model makes two important assumptions. The first is that the resource that is fetched is a text document. The second is that the resource represents a JavaScript file.

The simplest extension mechanism we can provide is to allow users to change what it means to 'execute' a resource. Currently, when a script is set to be executed this means that we create a <script> tag, inject the text of the file into it and then append it to the DOM so that the browser will execute the JavaScript inside. We can change this to instead look up a handler based on the 'type' property of the resource and to call that. The current script injection function would be the default handler.

var injectScript = function( obj ) {
	var script = document.createElement('script');
	script.defer = true;
	script.text = obj.data;
	head.appendChild( script );
};

var handlers = {
	default: injectScript
};

var execute = function( obj ) {
	if( obj.type && handlers[ obj.type ] ) {
		return handlers[ obj.type ]( obj );
	}

	return handlers.default( obj );
};

basket.addHandler = function( type, handler ) {
	handlers[ type ] = handler;
};

The type could either be specified by the user during the call to require or could be set to the Content-Type that was provided when the resource was fetched over the network. I will discuss this further below.

This mechanism will work for CSS, client-side templates, and other text data.

basket.addHandler( 'text/css', function( obj ) {
	var el = document.createElement( 'style' );
	el.setAttribute( 'type', 'text/css' );
	if ( el.styleSheet ) { // >= IE6
		el.styleSheet.cssText = obj.data;
	} else { // Rest
		el.appendChild( document.createTextNode( obj.data ) );
	}
});

var templates = {};

basket.addHandler( 'text/template', function( obj ) {
	templates[ obj.key ] = _.template( obj.data );
});

##JSON API responses##

One possible use-case for basket.js is caching API responses. If you were creating a Twitter clone, for example, you might want the list of tweets to be cached so that they can quickly be displayed when the page first loads. In the case where you also make your app offline capable (perhaps with Appcache...), the cached API responses can be used to show previously fetched data to the user rather than showing nothing.

The flow would be something like:

  1. Initially give me the cached response if you have one
  2. Wait for the application to finish bootstrapping and displaying the cached data
  3. Try to fetch a live response to replace the cached version with
  4. Future requests should be live

The problem with the current implementation is that if you use, for e.g., basket.require({ url: '/tweets/wibblymat' }); then it will always return the cached version if there is one. If you instead use a unique token it is difficult to determine what token should be used to fetch the cached version the next time the page is loaded.

You can currently use the isValidItem override like this:

var useCache = true;

basket.isValidItem = function( source, obj ) {
	// isApiCall would determine whether the request was for an API we wish to fetch from cache only on page load
	if( isApiCall( obj.url ) ) {
		return useCache;
	} else {
		return true;
	}
}


// Some time later, when the page is finished loading...
useCache = false;

Or you could use basket.get() to determine if the request was already cached

var response = basket.get( '/tweets/wibblymat' );
if( response ) {
	callback( response );
} else {
	basket
		.request({ url: '/tweets/wibblymat' })
		.then( function( responses ) {
			// the then() callback gives you an array of responses even when only requesting one item
			callback( responses[ 0 ] );
		});
}

These are both a little bit hackish. One alternative would be add another flag to the request object, something like live: true to say that we don't want to use the version from the cache even if it exists, but we do want to cache the result.

##Binary resource types##

There has also be some suggestion of storing images or other binary formats with basket.js. This is much more problematic because we currently use XHR.responseText to get at the data. We could expose another hook that allowed you to override the fetch with code of your own but we still need to serialize this data in order to store it in localStorage. Once you have overridden the network request, the storage serialisation and the action to take when the resource is fetched you are possibly better off not using basket.js.

Adding built-in support for arbitrary binary types is going to involve XHR2 features which rules out IE < 10.

We could add built-in handlers to fetch DOM supported types like images using new Image(), etc. but I don't believe that you can serialize the images to data URIs in IE < 10 either even though such a serialization can be used to produce in image in IE8.

I would recommend against binary data support unless we also wanted to drop IE9 support.

##Resource types/Content-Type##

Some servers return incorrect Content-Type headers for the resource being returned. In such a situation you want to be able to specify the type in the require() parameters. On the other hand, many servers do return the correct Content-Type, and in that situation it is more code (and therefore more potential for bugs) to have to set the type manually yourself when it could be determined from the server.

For this reason I propose that the type property of a resource will be the type parameter of the require() call if it is set, with fallback to the Content-Type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment