The Composum client libraries provide a mechanism to group resources like css and javascript libraries needed for a module together, including versioning and dependencies from one another, and provide an efficient mechanism to deliver these in compressed form to the browser by the ClientlibServlet.
Format of a client library
A client library is given as a sling:Folder with a sling:resourceType of composum/nodes/commons/clientlib that contains subfolders (typically of resourceType sling:OrderedFolder since the inclusion order is often important) for each resource type. Currently there are three resource types: js, css, link - other folders are ignored.
/apps/someapplication/somemodule/firstclientlib
+-- css
+-- cssmodule1
+-- something.css
+-- somefolder/somethingelse.css
+-- js
+-- jsmodule1
+-- jsmodule2
+-- somethingother.js
+-- link
+-- iconlink1
+-- iconlink2
The client library itself (here firstclientlib) can specify the following attributes:
category |
array of strings |
Alternatively to referencing client libraries by their absolute or relative path (relative to the search paths) it is possible to give a set of categories and reference these by their category. This way the client libraries can be extended by introducing a new client library with the same category. A category name has to match the regex [a-zA-Z0-9._-]+ . |
optional |
---|---|---|---|
order |
integer |
Specifies an ordering when client libraries are referenced by category: they are included in increasing order. Default 0, but it is recommended to specify an order attribute when category is given. To leave the possibility to put client libraries later in the middle of the existing ordering of client libraries of the same category, it is recommended to have this a multiple of 1000. |
optional |
Each of the resource folders (js,css,link) can specify resources that need to be included into the page with the following optional properties. For modularization it is also possible to put other children into the resource folders who declare additional embeddings or dependencies.
depends |
array of resource patterns |
An array of dependencies that have to be included into the page, too. This can specify either other client libraries, or resources like javascript or css files. It is possible to give patterns to define version ranges for e.g. javascript libraries. If such a resource was already included into the rendered resource this client library is rendered by tag cpp:clientlib (see below), the dependency is satisfied and thus ignored. Otherwise it is included into this client library. It is also possible to reference categories e.g. as category:composum.assets.commons or external URLs (http / https / protocol omitted). |
optional |
---|---|---|---|
embed |
array of resources |
The resources that belong to the client library. These are output with the cpp:clientlib tag. In case of CSS and javascripts these are possibly concatenated (if not forbidden by the expanded property) by the ClientlibServlet to minimize the number of requests. |
optional |
optional |
boolean expression |
If false, a warning is logged if one of the client libraries resources cannot be found. |
optional, default: false |
expanded |
boolean expression |
If true, the resources will not be appended into one file, but served separately. Relevant for types css and js only. |
optional, default: false |
For example, the resource folder js could have the following attributes:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OrderedFolder"
optional="{Boolean}false" expanded="{Boolean}false"
depends="[https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.7/ace.js,
jslibs/jquery/([1-3]*:3.4.1)/jquery.js,
jslibs/underscore/(1.*:1.9.1)/underscore.js,
jslibs/bootstrap/(3.*:3.4.1)/js/bootstrap.js,
composum/nodes/commons/components/clientlibs/components]"
embed="[category:composum.assets.commons,
composum/assets/commons/widgets/asset/finder/finder.js,
composum/assets/commons/dialogs/asset/select/select.js]"/>
Clientlib resolution
A clientlib can be referenced by the absolute path to the clientlib top node (in the example /apps/someapplication/somemodule/firstclientlib), by a relative path wrt. to the resource resolver search path (relative path resolution, by default /apps then /libs), or by categories (see below). If there are several client libraries with the same relative path wrt. to the search path, the first one will hide the others. E.g., if the search path is /apps,/libs, a client library /apps/some/thing will hide a client library /libs/some/thing.
It is recommended to store the version of javascript / css libraries as part of the path - e.g. /libs/jslibs/jquery/2.2.4/jquery.js to enable a stepwise upgrade of the application. A client library can specify a pattern of versions of the library which it is compatible with, and a default version that is included when there isn't already a compatible version of the library included into the rendered resource. The general format for such a range of versions is
(<versionpattern>:<defaultversion>)
where versionpattern can be a string ending with a * (which matches any string not containing a slash) or it can contain character ranges like [1-3], matching characters 1 to 3. For example, ([1-3]*,2.2.4) matches any version starting with 1,2 or 3; if no version of the library was included yet we should use 2.2.4. The pattern (1.8.*:1.8.3) means we can use any version starting with 1.8. but will use 1.8.3 if there wasn't already a version of that library included.
To enable application-specific extensions to (even predefined) client libraries, the client libraries can be referenced in the depends and embed attributes and in the path attribute to the cpp:clientlib tag as e.g. category:examplecategory. This searches all search path entries for all client libraries with an attribute category (multi string) that contains the named category (here "examplecategory"). The thusly referenced client libraries are processed in the order given by the attribute order (an integer) - least are processed first. If there are several client libraries with the same relative path wrt. to (different) search path entries, the first one will hide the later.
Request count reduction, handling of minified versions and the development (debug) mode
The client libraries can deliver both minified and un-minified versions of the CSS / JS. The module takes care of automatical minification of CSS using the YUI Compressor. For Javascript files there is no automatic minification available; here we have the convention that a minified version should be available with the extension .min.js whereas the un-minified version should be available with the extension .js. (Such minified versions can for instance be generated by the Minify Maven Plugin.) If only one of these versions is present, it is always used.
Libraries that have only a minified version should be available as a resource with the appropriate selector, e.g. .min.js, but should be referenced without the .min selector when included as dependencies or embedded files. (Rationale: when later an unminified version is introduced, too, that shouldn't change the client libraries.)
Furthermore, the request count for CSS / JS is minimized by appending all css or js files into one file. The request count minimization can be switched off for a client library or a resource folder with the expanded attribute.
During development, both behaviours (minification and request count minimization) can be switched of by the OSGI configuration debug of the clientlib module. This has the same effect as if every client library has expand=true on its resource folders.
cpp:clientlib
A tag that specifies a reference to a client library and outputs links to the resources of the given type (e.g. CSS, javascript) referenced by the client library. Compare also Page Rendering examples: for css resources it is usually used in the head of a document to output the links to the CSS files and at the end of the document body to output links to the javascript files.
type |
link, css, js |
type of the resources that should be linked / written. Default: link. |
optional |
---|---|---|---|
path |
path to the client library |
the path to the client library - a folder with sling:resourceType composum/nodes/commons/clientlib containing subfolders for the individual types. This can be an absolute path, a relative path wrt. the resource resolver search path, or a category declaration category:examplecategory. |
optional |
category |
category of the client libraries |
alternatively to path, a category of client libraries can specified. All client libraries with this value in their attribute "category" are included, ordered by their attribute "ordering". |
optional |
Rendering of client libraries
When rendering a client library or category is requested (cpp:clientlib tag), it is located by its path (see clientlib resolution) and its resource folder named according to the requested type is rendered as follows. In case of resolution via categories, the resolved client libraries are processed consecutively in the order as specified by their order attribute. For all files declared as dependencies or embedded, version ranges are applied. If the clientlib debug configuration is active (true), everything is treated as if the expand attribute is true. If optional is false, a warning is logged if something is not found. In each case, the rendered item is processed only if the resource was not already rendered in the current request (class RendererContext ) - for brevity this is not explicitly mentioned.
- All client libraries / resources contained in its depend attribute are processed: if it is a file, a link to it is rendered. if it is a client library, it is processed and rendered.
- All client libraries / resources contained in it's embed attribute are processed: if it is a file, a link to it is rendered iff expand is true. (Otherwise, it is embedded into the response by the ClientlibServlet/ClientlibCategoryservlet for the request to the client library rendered later.) if it is a client library or -category, its content is processed recursively. If this client library was already rendered, an error is logged, since this client library will be included twice into the page.
- All other folders or resources contained in it are processed (if needed, an ordering can be specified by using sling:OrderedFolder): if it is a file, a link to it is rendered iff expand is true. (Otherwise, it is embedded into the response by the ClientlibServlet/ClientlibCategoryservlet for the request to the client library rendered later.) if it is another resource, it is rendered recursively. That is, its embed and depends attributes are read and their contents rendered as given here; if it has expands and optional attributes, these override these settings for the currently rendered item.
Last, a request to the ClientlibServlet is rendered for the client library if there is any embedded content, or to the ClientlibCategoryServlet if that's appropriate. (This is not done in the recursive case, unless mentioned in the previous steps.)
Links to files (either because they are given as depend or with expand is true) are only rendered if there it was not not already a link rendered in the page (possibly matching the version range).
Delivery of client libraries
The ClientlibServlet delivers the concatenated embedded content of client libraries for extensions js / css for which expand is false. The embedded content is concatenated in the order it is processed by the rendering algorithm. If the client library / category depends directly or indirectly (via client libraries / categories it depends on) on any files it also directly or indirectly embeds, these are omitted from embedding, and a warning is logged.
For the delivery of concatenated embedded content of categories of client libraries there is the ClientlibCategoryServlet which matches the path /bin/cpm/nodes/clientlibs(.min).<type>/category .
Versioning of client library cache content
To ensure that a request for a client library content (inclusive its embedded contents) is always served with the newest cache content, regardless of any browser or HTTP-proxy caches between the application and the browser, we append a hash in an artificial suffix of the URL to the ClientlibServlet or ClientlibCategoryServlet containing this hash. Whenever the client library contents change, the hash (as calculated during the rendering process) will change also, such that all caches are forced to update their contents, since they encounter an URL they haven't seen so far. Any requests to old versions of the client library are served with a redirect to the newest version.
URL: ...something.min.js/<hash>/something.min.js
Algorithm:
- If the hash is consistent with the hash saved in the cache file, the cached file is served if the user has permissions to read the client library (or any of the client libraries of a category).
- If the hash is not consistent with the hash saved in the cache file, and there is an If-Modified-Since header older than the last-modified of the cache file, we just assume the request comes from an obsolete cached version of a page referring to the client library, and serve a permanent redirect to the cached version without checking the client library.
- Otherwise we check the client library to determine the last hash and update time, update the cache file if this is changed wrt. the cached version and serve a permanent redirect to the new version. (Possible improvement: in the future the redirect could be served in parallel to the regeneration of the cache file).
For files there is no such mechanism implemented, since they have an obvious last modification date. A reduction of transfer volume can be reached by, say, given them a Cache-Control header with a maximum age of a day, and relying on If-Modified-Since to further reduce the bandwidth.
Gotchas and possible errors
- If a user has permissions to read only some parts of a client library, there are warnings logged (if they aren't optional). If a client library is visible for the use, but some embedded parts are invisible, they will be delivered by the ClientlibServlet, anyway, since its result is cached for all users. Please avoid different permissions for a client library and embedded parts - it's probably best to have everything public. This might also have a serious performance impact, since this triggers a up to date check for client library on every delivery, since the hash generated by the cpp:clientlib tag will reflect only the parts visible to the user.
- If a client library was rendered (possibly by recursive embedding / dependencies from the currently rendered client library) and is embedded in the currently rendered client library, this is an error since it will be included twice into the page.
- If a resource is not found, a warning is logged unless the resource / folder including it was marked as optional.
- Embedding of several versions of one resource cannot be reliably detected in all cases. That can occur when you have transitive dependencies between client library that declare different version ranges.
Logged warnings and errors
Typ |
Vorgang |
Bedingung |
---|---|---|
Warnung |
Rendering / Delivery |
A resource was referenced that was not found and not declared optional. |
Error |
Rendering |
A link to a client library or category is rendered that embeds files for which either links are already rendered or which are embedded into already rendered links to client libraries or categories. |
Warning |
Delivery |
The delivered client library or category wants to embed a file to which is embedded into any of its dependencies / is a dependency. This file is not embedded, so the page should work, anyway, but this is a hint that the dependency structure probably unfortunate. |
Tips and Tricks
Example X-Paths for searching the repository e.g. with the browser:
- all client libraries: //element(*,sling:Folder)[@sling:resourceType='composum/nodes/commons/clientlib'] order by @path
- all client libraries below /libs with some categories: /jcr:root/libs//element(*,sling:Folder)[@sling:resourceType='composum/nodes/commons/clientlib'][@category='clientlib1' or @category='clientlib2'] order by @path