Custom Proxies
One of the first things I do when I start work on a new ExtJS application is to write an application-specific proxy. It's a powerful technique and one I'd highly encourage anyone working with ExtJS to adopt. Here I try to introduce the core concept and then demonstrate some of the more reusable patterns that I've found useful in my own custom proxies.
A Simple Custom Proxy
Let's start with an example. The code below shows the proxy configuration for a new store. There's nothing particularly noteworthy about this configuration, it should look pretty similar to the kind of thing you've seen many times before in your own applications.
Ext.create('Ext.data.Store', { ... proxy: { type: 'ajax', limitParam: 'count', pageParam: null, url: '/public/server.json', reader: { root: 'data', type: 'json' } } });
On its own there's nothing wrong with this proxy but the problems start as soon as the application starts to grow. As new stores get added you find yourself copying and pasting this same proxy configuration over and over again with only minor modifications.
The way to avoid the duplication is to move all of the shared configuration into your own proxy class. That might sound a little daunting if you haven't done it before but it's actually quite easy.
Let's start with a really simple proxy subclass. It extends the built-in Ajax proxy and is functionally identical to it. This is just a starting point, we'll be adding useful functionality later.
Ext.define('MyApp.proxy.MyAppProxy', { alias: 'proxy.myapp', extend: 'Ext.data.proxy.Ajax' // Ajax, Rest or JsonP, as appropriate });
An important thing to note is the alias. It allows this class to be used as a proxy type. So if we wanted to change our earlier example to use this new proxy it'd look like this:
Ext.create('Ext.data.Store', { ... proxy: { // This will create a proxy using the alias 'proxy.myapp' type: 'myapp', // The rest of the configuration hasn't changed from using an Ajax proxy limitParam: 'count', ... } });
The next step is to move some of the configuration options onto our new proxy class. Simple options like limitParam can just be copied straight across but the reader needs to be set in the constructor to avoid problems with shared references.
Ext.define('MyApp.proxy.MyAppProxy', { alias: 'proxy.myapp', extend: 'Ext.data.proxy.Ajax', limitParam: 'count', pageParam: null, constructor: function() { this.reader = { root: 'data', type: 'json' }; this.callParent(arguments); } });
The proxy configuration on the store now looks much simpler:
Ext.create('Ext.data.Store', { ... proxy: { type: 'myapp', url: '/public/server.json' } });
The idea is that we can now share this custom proxy class with many other stores in the same application without needing to keep repeating all the configuration options every time. In this example the url is still specified on each instance as that varies between stores.
One common use for a custom proxy is to remove some of the clutter from the requests that are made to the server. By default various parameters get added to the URL such as paging and sorting details. These can be disabled across an entire application via its proxy:
Ext.define('MyApp.proxy.MyAppProxy', { alias: 'proxy.myapp', extend: 'Ext.data.proxy.Ajax', directionParam: null, filterParam: null, groupDirectionParam: null, groupParam: null, limitParam: null, pageParam: null, sortParam: null, startParam: null });
If an individual proxy instance wants to add these parameters back in it can override these values in its configuration with the parameter names to use:
Ext.create('Ext.data.Store', { ... proxy: { type: 'myapp', pageParam: 'page', ... } });
We can now start to explore some of the more advanced techniques available for customizing the requests made by our proxy. The URL is a rich source of improvements as the URLs within a single application often follow some sort of pattern that we can build into our proxy.
Overriding buildUrl
- Update: The default implementation of buildUrl is responsible for adding the cache-buster parameter. This is ignored in some of the examples below. If you need the cache-buster then you should consult the source code for the original implementation for more information. The method getUrl is potentially a useful override point to avoid this problem but it is technically a private method.
Let's consider an application where the request URLs look like this:
/public/group.json /public/product.json /public/user.json
While we could specify the whole URL each time, we can do better. The pattern here is:
/public/{category}.json
Rather than specifying the whole URL it'd be better if we could just specify the part that changes. Ideally we'd want the proxy configuration to look something like this:
Ext.create('Ext.data.Store', { ... proxy: { category: 'user', type: 'myapp' } });
So how do we achieve it? It turns out to be a simple override of the buildUrl method in our custom proxy:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildUrl: function(request) { return '/public/' + encodeURIComponent(this.category) + '.json'; } });
The buildUrl method is called when the proxy is preparing to make a request to the server. It is passed an instance of Ext.data.Request representing the request, though in this example we didn't need to use it. The string returned by buildUrl is then used as the URL for the request.
Behind the scenes, all of the configuration options for a proxy are copied onto the new
proxy instance. The vast majority of ExtJS classes behave this way, performing an
Ext.apply(this, config)
somewhere near the start of their constructor.
So the category: 'user'
configuration option becomes a category
property on the proxy that can be accessed via this.category
in
buildUrl.
Another system used in many applications is to keep the core URL fixed and to use one of the request parameters to denote the request type, like this:
/public/server.json?category=product /public/server.json?category=user /public/server.json?category=group
The pattern here is:
/public/server.json?category={category}
One way we could implement this would be to adjust our existing buildUrl example:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildUrl: function() { return '/public/server.json?category=' + encodeURIComponent(this.category); } });
However, this implementation doesn't honour the url or api configuration options, so arguably it would be better to write it like this instead:
Ext.define('MyApp.proxy.MyAppProxy', { ... url: '/public/server.json', buildUrl: function() { return Ext.urlAppend(this.callParent(arguments), 'category=' + encodeURIComponent(this.category)); } });
Overriding buildRequest
The last example works fine for simple URL manipulation but if we're using POST requests we might want the category parameter to be in the request body along with all the other parameters rather than hanging it off the end of the URL. There are a couple of approaches we could use for this. The first uses the proxy's extraParams:
Ext.define('MyApp.proxy.MyAppProxy', { ... constructor: function() { this.callParent(arguments); this.setCategory(this.category); }, setCategory: function(category) { // setExtraParam didn't exist until ExtJS 4.1.0 this.setExtraParam('category', category); } });
While this works fine in simple cases, a more powerful technique is overriding the buildRequest method, not to be confused with the buildUrl method we saw earlier:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildRequest: function(operation) { var request = this.callParent(arguments); request.params.category = this.category; return request; } });
Even though it isn't used in this example, the operation parameter passed to buildRequest is worthy of note. The class Ext.data.Operation is used to wrap the many options that a store needs to pass to a proxy to load data. Among other things it includes details about paging, sorting and filtering. It is the proxy's responsibility to figure out the correct way to load the data for a given operation.
For example, let's imagine we have a server that handles paging via two parameters, start and end, specifying the indexes of the first and last record to load. To make things slightly trickier, let's assume that the server expects these parameters to be one-based rather than zero-based. There isn't any support for this built into the Ajax proxy but it can be added quite easily by mapping the options in the operation to request parameters:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildRequest: function(operation) { var request = this.callParent(arguments), params = request.params; // Operation is zero-based, our request parameter is one-based params.start = operation.start + 1; // Calculate an end parameter from the properties of the operation params.end = operation.start + operation.limit; return request; } });
- Update: For those using ExtJS 4.2.2 or above, the scenario described in the next example would be better solved using the paramsAsJson config setting.
As another example, consider a web service that expects its parameters to be encoded as a JSON payload in the body of a POST request. This can also be achieved relatively easily using a custom buildRequest:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildRequest: function(operation) { var request = this.callParent(arguments); // For documentation on jsonData see Ext.Ajax.request request.jsonData = request.params; request.params = {}; return request; }, getMethod: function(request) { return 'POST'; } });
In this example we have also overridden the method getMethod to ensure that all requests use POST. While its use may be a bit gratuitous in this simple example, it can be a useful method to override in more advanced proxies. The request passed to getMethod is the same object that was returned by buildRequest.
Token Substitution
The earlier example of overriding buildUrl works fine for a simple URL pattern but it is a little inflexible. In many applications there isn't a single pattern for all the URLs, there may be several depending on the context. We could create a subclass of our custom proxy for each of the different patterns but it can prove simpler to use a templating system.
Let's consider the kind of configuration we'd like to have for our new proxy:
Ext.create('Ext.data.Store', { ... proxy: { category: category, type: 'myapp', url: '/product/{category}/list' } });
We have a token in the URL and we'd like to have it replaced in the final request. There's nothing to do this out-of-the-box but it can easily be added in our custom proxy class:
Ext.define('MyApp.proxy.MyAppProxy', { ... buildUrl: function(request) { var url = this.callParent(arguments); return this.replaceTokens(url); }, replaceTokens: function(str) { var me = this; // Find and replace using a RegExp return str.replace(/{(.*?)}/g, function(full, token) { return encodeURIComponent(me[token]); }); } });
If you find regular expressions innately scary then this example might have got you a bit flustered but they do provide a concise way to perform the substitution. The regular expression is used to find the tokens in the URL and the replacement values are then read off the proxy.
This style of token substitution also works nicely with a proxy's api configuration option. For example, let's switch our attention from stores to models and consider a CRUD scenario where the URLs follow the pattern shown here, including the id of the relevant model:
CREATE /group/add READ /group/135/load UPDATE /group/135/save DESTROY /group/135/remove CREATE /user/add READ /user/749/load UPDATE /user/749/save DESTROY /user/749/remove
The id in these examples is missing in the CREATE case as the id isn't known until after the record is saved. We might want the corresponding proxy configuration on the User model to look a bit like this:
Ext.define('MyApp.model.User', { extend: 'Ext.data.Model', ... proxy: { category: 'user', type: 'myapp' } });
There are a number of ways this could be implemented using the techniques we've seen previously but if we wanted to use token substitution it might look something like this:
Ext.define('MyApp.proxy.MyAppProxy', { ... api: { create: '/{category}/add', read: '/{category}/{id}/load', update: '/{category}/{id}/save', destroy: '/{category}/{id}/remove' }, buildUrl: function(request) { var url = this.callParent(arguments); return this.replaceTokens(url, request); }, replaceTokens: function(str, request) { var me = this; return str.replace(/{(.*?)}/g, function(full, token) { // We read the id from the request params, the category is read from the proxy itself return encodeURIComponent(request.params[token] || me[token]); }); } });
The replacement value for category is read from the proxy just like in the
previous example but the model id will need to vary for each request. By
default the id gets added to the request parameters so we can access it using
request.params.id
.
Given the diversity of web applications it's unlikely that any of my examples will be a perfect match for your own needs. Hopefully the various techniques I've covered will allow you to find a suitable starting point in writing a custom proxy of your own. If nothing else I hope you'll agree that an application-specific proxy is a something well worth having.
Update: ExtJS 5
Using a custom proxy is still a valid technique with ExtJS 5 but you may wish to consider configuring a Schema instead. In some cases this allows a model to omit the proxy configuration altogether. The Schema supports simple templating to generate a suitable proxy automatically.
By default the Schema will set the url for the proxy based on the name of the model. Unfortunately, this will override any url specified on the proxy class itself. I have filed a bug report but if you need a workaround you can call setUrl from within the proxy's constructor.
Similarly, if you want to set the reader from within the constructor you'd have to call setReader rather than assigning it directly to the property. I regard this change in behaviour as a bug and have filed a bug report. However, the original shared reference problems that were present in ExtJS 4 are fixed, so you can specify the reader directly on the class instead:
Ext.define('MyApp.proxy.MyAppProxy', { alias: 'proxy.myapp', extend: 'Ext.data.proxy.Ajax', ... // No need for a constructor reader: { root: 'data', type: 'json' } });