As Dojo moves toward its 2.0 release, our focus has been on giving developers tools that will help them be productive in any JavaScript environment. This means creating consistent APIs across all environments. One area that has been sorely lacking, in this regard, is Dojo’s IO functions. We’ve always provided developers with a way to make requests in the browser (dojo.xhr*
, dojo.io.iframe
, dojo.io.script
), but the API has been less consistent than some of us would like (dojo.xhrGet
, dojo.io.script.get
, etc.). Additionally, we’ve never provided a server-side implementation, and if we had, it would have been another module name and API call to remember.
With the release of Dojo 1.8, we have introduced the dojo/request
API which provides consistent API calls between browsers, request methods, and environments:
require(["dojo/request"], function(request){
var promise = request(url, options);
promise.then(
function(data){
},
function(error){
}
);
promise.response.then(
function(response){
},
function(error){
}
);
request.get(url, options).then(...);
request.post(url, options).then(...);
request.put(url, options).then(...);
request.del(url, options).then(...);
});
The function signature for dojo/request
and all providers is a URL and an object specifying options for the request. This means using dojo/request
is as easy as passing it a string argument; the options
argument is truly optional. Let’s take a look at the common properties you can pass on the options
object:
- method – HTTP method to use for the request (default is
'GET'
; ignored bydojo/request/script
) - query –
key=value
string or{ key: 'value' }
object specifying query parameters - data – string or object (serialized to a string with
dojo/io-query.objectToQuery
) specifying data to transfer (ignored byGET
andDELETE
requests) - handleAs – string specifying how to handle the server response; default is ‘text’, other possibilities include ‘json’, ‘javascript’, and ‘xml’
- headers –
{ 'Header-Name': 'value' }
object specifying headers to use for the request - timeout – integer specifying how many milliseconds to wait before considering the request timed out, canceling the request, and rejecting the promise
The consistency of the API also extends to its return value: all dojo/request
methods return a promise that will resolve to the data contained in the response. If a content handler was specified when the request was made (via the handleAs
option), the promise will resolve to the result of the content handler; otherwise it will resolve to the response body text.
Promises returned from dojo/request
calls extend normal dojo/promise
behavior with an additional property: response
. This property is also a promise that will resolve to a frozen object (where available) describing the response in more detail:
- url – final URL used to make the request (with query string appended)
- options – options object used to make the request
- text – string representation of the data in the response
- data – the handled data in the response (if
handleAs
was specified) - getHeader(headerName) – a function to get a header from the request; if a provider doesn’t provide header information, this function will return
null
.
The example at the top of this post shows this in action through the use of promise.response.then
Providers
Behind the scenes, dojo/request
uses providers to make requests. For each platform, a sensible default is chosen: browsers will use dojo/request/xhr
and Node will use dojo/request/node
. It should be noted that newer browsers (IE9+, FF3.5+, Chrome 7+, Safari 4+) will use the new XMLHttpRequest2
events instead of XMLHttpRequest
‘s onreadystatechange
that is used in older browsers. Also, the Node provider uses the http
and https
modules, which means no XMLHttpRequest
shim needs to be employed on the server.
If a provider other than the default needs to be used (for instance, the provider for JSON-P), there are three choices available: use the non-default provider directly, configure it as the default provider, or configure the request registry.
Because all providers conform to the dojo/request
API, non-default providers can be used directly. The approach taken with the dojo/request
API is analogous to the approach of dojo/store
. This means that if you only have a few services that return JSON-P, you can use dojo/request/script
for those services without having to change the basic API signature. Using a provider this way is slightly less flexible than the other two choices (especially for testing), but is a completely valid way to use a non-default provider.
Another way to use a non-default provider is to configure it as the default provider. This is helpful if we knew that our application was only going to use one provider that wasn’t the default. Configuring the default provider is as simple as setting a provider’s module ID as the requestProvider
property of dojoConfig
:
<script>
var dojoConfig = {
requestProvider: "dojo/request/script"
};
</script>
<script src="path/to/dojo/dojo.js"></script>
requestProvider
can also be set up via data-dojo-config
like any other configuration parameter. In addition, any function that conforms to the dojo/request
API can be used as the default provider. This means we could develop a custom module that wraps dojo/request/xhr
, adds additional headers for authentication, and configure it as our application’s default provider. During testing, a separate provider could be used to simulate responses from the server to test if our application is making requests to the correct services.
Although configuring the default provider gives us more flexibility than using providers directly, it still doesn’t give us the flexibility needed to use one API call (dojo/request
) with different providers based on a specified criteria. Let’s say our application’s data services needed one set of authentication headers for one service and an entirely different set of headers for a second service. Or JSON-P for one and XMLHttpRequest
for another. This is where using dojo/request/registry
shines.
Registry
One module that has been present in DojoX for a long time, but is not widely used is dojox/io/xhrPlugins
. This module provides a way to use dojo.xhr*
as the interface for all requests, whether those requests needed to be made via JSONP, iframe, or another user-defined provider. Because of its usefulness, the idea has been adapted as dojo/request/registry
.
dojo/request/registry
conforms to the dojo/request
API (so it can be used as a provider) with the addition of the register
function:
// provider will be used when the URL of a request
// matches "some/url" exactly
registry.register("some/url", provider);
// provider will be used when the beginning of the URL
// of a request matches "some/url"
registry.register(/^some\/url/, provider);
// provider will be used when the HTTP method of
// the request is "GET"
registry.register(
function(url, options){
return options.method === "GET";
},
provider
);
// provider will be used if no other criteria are
// matched (a fallback provider)
registry.register(provider);
If no criteria are matched and a fallback provider hasn’t been configured, the default provider for the environment will be used. Since dojo/request/registry
conforms to the dojo/request
API, it can be used as the default provider:
<script>
var dojoConfig = {
requestProvider: "dojo/request/registry"
};
</script>
<script src="path/to/dojo/dojo.js"></script>
<script>
require(["dojo/request", "dojo/request/script"],
function(request, script){
request.register(/^\/jsonp\//, script);
...
}
);
</script>
This is great if we want to use the platform’s default provider (XHR for browsers) as our fallback. We could also set up a fallback provider using the last API call above, but no other providers could be registered afterward. Instead, dojo/request/registry
can be used as a plugin from requestProvider
to set the fallback provider:
<script>
var dojoConfig = {
requestProvider: "dojo/request/registry!my/authProvider"
};
</script>
<script src="path/to/dojo/dojo.js"></script>
<script>
require(["dojo/request", "dojo/request/script"],
function(request, script){
request.register(/^\/jsonp\//, script);
...
}
);
</script>
Now, any request not matching the criteria we have set up will use the module located at my/authProvider
.
The power of the registry may not be readily apparent. Let’s take a look at some scenarios which make the benefits stand out. First, let’s consider an application where the server API is in flux. We know the end-points, but we don’t know what headers will be required or even what JSON objects will be returned. We could easily set up a registry provider for each service, temporarily, and start coding the user interface. Let’s say we guess that /service1
will return items as JSON in an items
property and /service2
will return them as JSON in a data
property:
request.register(/^\/service1\//, function(url, options){
var promise = xhr(url, lang.delegate(options, { handleAs: "json" })),
dataPromise = promise.then(function(data){
return data.items;
});
return lang.delegate(dataPromise, {
response: promise.response
});
});
request.register(/^\/service2\//, function(url, options){
var promise = xhr(url, lang.delegate(options, { handleAs: "json" })),
dataPromise = promise.then(function(data){
return data.data;
});
return lang.delegate(dataPromise, {
response: promise.response
});
});
All service requests in the user interface can now be used in the form request(url, options).then(...)
and they will receive the proper data. As development proceeds, however, the server team decides that /service1
will return its items as JSON in a data
property and /service2
will return its items as XML. Without using the registry, this would have been a major code change; but by using the registry, we’ve decoupled what our widgets and data stores are expecting from what the services are providing. This means the server team’s decisions only cause code to change in two places: our providers. Potentially, we could even decouple our user interface from URLs altogether and use generic URLs which the registry then maps to the correct provider that uses the correct server end-point. This allows end-points to change without the pain of updating code in multiple places.
This decoupling can also be extended to testing. Usually during unit tests, a remote server is not desirable: data can change and the remote server could be down. This is why testing against static data is recommended. But if our widgets and user interface have end-points or request calls hard-coded into them, how do we test them? If we’re using dojo/request/registry
we simply register a provider for an end-point that will return static data for requests for our unit tests. No API calls need to change. No code within our application needs to be rewritten.
Conclusion
As you can see, dojo/request
was written with the developer in mind: a simple API for simple situations, but flexible options for the most complex applications.
Resources
To learn even more about dojo/request
, check out the following resources: