Skin Extensions (SX)

Last modified by Vincent Massol on 2024/02/26 17:57

 XWiki
 Implementation
 Completed
 

Description

Currently, when someone wants to add a new feature that requires custom scripting or custom css, these are globally added to the list of loaded javasript files (in javascript.vm) and css files (in style.css). This means that even if a feature is rarely seen by users, the browser has to transfer the style and client-side functionality of that feature. And given the fact that there are already several such features, and there will be even more, we're needlessly increasing the transfer size and page loading time.

Considering that every piece of code/style is used for some part of the interface, which might be completely disabled in a wiki, or might be only sometimes present, instead of pushing possibly needed code, the platform should let the interface components pull their needed js or css code.

JavaScript Extensions (JSX), StyleSheet Extensions (SSX)

JSXs and SSXs are stored as standard XObjects. Usually, an Application or an Interface Component will have one or more XClasses, with their templates and sheets, some navigational documents with scripts processing the structured documents, an internationalization bundle document, some interface extensions and some skin extensions. A document can store more than one skin extension, but they are not separable, meaning that when an application requests for a JSX, it uses that extension's document name, and all the JSX objects found in that document are sent to the client in one response. The benefit of having more than one extension in a document is that the code can be separated into distinct "files" (=objects), and the server compacts all those files into one response.

Reasons why we shouldn't differentiate objects in the same document:

  • Simpler URLs; using the object number would require an extra query parameter
  • Simpler extension identification; a properly chosen document name is stable, while the object number can be changed and has no meaning
  • If we need more extension, we can just use more documents, it's not like we're running out of available document names

Skin extensions are added on request, while parsing the content. Basically, an interface component signals that it needs certain CSS or JavaScript extensions, which are later introduced in the html header.

JSX/SSX class

  • name: string
  • code: textArea
  • use: staticList: onDemand, always ([alternative] global: boolean: yes, no)
  • parse: boolean: yes, no [optional, to be decided]
  • cache: boolean: yes, no ([alternative] staticList: long, short, never, forbid) [optional, to be decided]

Each extension object has a name, which can be used for displaying the extension in an extension editor, for example, or in the logs. It is needed because more than one extension can be in a document, so the document name is not enough for distinguishing an extension.

The code of the extension is the content that gets sent to the client. For a JSX, it is some JavaScript code, while for a SSX it is some css code. It could also contain velocity code (see the "parse" property below).

While most of the skin extensions will support an application or an interface component, being loaded onDemand when that application/component is used, some features could be provided only by javascript (like "double-click edit" enabled by javascript onload hooks), and some skin features could be made only by different CSS styling (like changing the position of the breadcrumbs). These features could be enabled by an empty interface extension that just pulls the skin extensions, but that is a waste of resources (more objects, bigger documents, more calls, larger caches). Instead, we can mark an SX as global, by saying that it should always be used. Global SX are automatically added to the list of used extensions.

Some extensions might require some velocity scripting in them. An use case for this is generating CSS based on some velocity variables. Another use case would be to generate a JavaScript based navigation menu, according to a structure defined in the wiki. For this, a SX can specify that it wants to be parsed. If more than one extension are in a page, then each object respects its parse property.

Since a js or css file is not supposed to change (unless the extension is updated with a new version), it should be cached on the client. However, there might be some exceptions for this rule. For example, in the example above with the javascript-based navigation menu, the resulting javascript file should not be cached for a very long time, as this will prevent any updates in the navigation structure to be observed on the client. For this, a SX could specify a different cache policy. Besides the default long cache, it could also specify that it should be cached only for a short period of time (in which case we would have to define what short really means), or that it must never be cached, as the content will always be different. If more than one extension are in a document, then the most restrictive value is used for the whole result (i.e. one never cache value will force the resulting composed file to not be cached). A special value is forbid, which actually forbids clients and proxies to cache this file, as it might contain sensitive information. Forbid is different from never because never will not send any cache headers, while forbid will send headers forbidding caching.

A means to force a cache refresh is to add a content hash in the URL, so whenever the extension content changes, the URL is also changed, thus forcing a new request for that file.

Implementation details

Since the javascript and css import declarations must be in the html header, and the extensions are pulled while generating the html content, it is obvious that the import statements must be gathered during the rendering process, and inserted afterwards.

The gathering process can be done using a process similar to the TOC generation. During the rendering phase, we gather the pulled extensions in a list (actually an ordered set, since an extension can be pulled in more than once, and we don't want to include it twice in the header; and really there are two sets, one for each type of skin extension, scripting or styling).

Two new classes, JsxHelper and SsxHelper should be created in com.xpn.xwiki.util, and added to the velocity context in the prepareContext phase ().

Extensions can be pulled in two ways:

  • An imperative one, using velocity. Something like $jsx.use("XWiki.AjaxTable"), $ssx.use("xwiki:Panels.PanelWizard"). These calls will add the parameters to the respective extension lists. As an example, inside the Panels.PanelWizard document content we would put $jsx.use("Panels.PanelWizard").
  • And a declarative one. We can extend the IX objects with two more fields, "jsx" and "ssx", which declare a comma-separated list of SXs that are needed whenever this IX is used. Later, when the IX controller detects that an IX must be used, it also adds its SX requirements to the lists.

Initially, the two lists can be filled with the global extensions, which can be cached for better performance.

After the rendering process is over, the list of extensions is transformed into proper HTML code, and inserted in the html header (if it exists). For this, a simple regex replace should do the job, something like renderedResult = renderedResult.replaceAll("_JSX HOOK HERE_", jsxString); this is not safe, as anyone can use that string somewhere in the content, but is a lot simpler than other solutions.

The links should look like:

"<script type=\"text/javascript\" src=\"" + xwiki.getURL(jsxDocName, "jsx") + "\"></script>\n";
"<link href=\"" + xwiki.getURL(ssxDocName, "ssx") + "\" rel=\"stylesheet\" type=\"text/css\"/>\n"

The links will point to new Struts Actions, /jsx/ and /ssx/. A complete URL would look like /xwiki/bin/jsx/Panels/PanelWizard. These actions output all the jsx/ssx objects found in the document, respecting the parse and cachePolicy properties. The result should also be minified using the yui compressor (according to this page this seems to be the best compression technique, since gzip compression strains the processor). If the extension can be cached on the client, then the minified result should be cached on the server, as the minification process is expensive, too.

Caches can be set using response.setHeader("header", "value"). See Caching in HTTP and Headers definition. We must use cache headers both for proxies and clients.

The actions could support an optional query param, ?minify=false, that disables minification (for debug purposes).

Usage

Since Skin Extensions are much easier to implement than InterfaceExtensions, they should be implemented as soon as possible and used in templates and XWiki documents.

This raises the question what should be a standard skin feature, and what should be an optional component. If we move features outside skins, this will mean that they will require their own Jira projects, build modules and release cycles. This can be seen as an advantage, as we don't have to release a new version of XE for a change in a skin feature. But until we have an easy application update process, this can also be seen as a disadvantage, as admins will usually install only new versions of XE, and not of individual apps.

All the javascript files, except prototype, and all the custom css files (like usersandgroups.css, lightbox.css) should be removed from the skin and placed in SkinExtensions, packaged as Applications bundled in products. This will allow reusable skin features without forcing them to be part of a skin or of the platform templates, and to be used in older version of XWiki without upgrading the whole skin (not older now, but versions that will support skin extensions and will be old in the future).

If a SX is general, it should be posted in its own document. If it is particular to a document in an application (like the PanelWizard script), it should be attached to the page using it.

Open question 1: Should $jsx.useFile("filename.js") work for files located on the disk? This allows the same pull process to be used with files located in the skin, without requiring SX documents and objects. I'd say yes. Then, what should the URL look like? /xwiki/bin/jsx/skins/albatross/somestyle.css is OK?

Open question 2: This also affects IX. How to specify dependencies? An extension could list some other extensions as requirements, and when using it, also add its dependencies to the list of used extensions.

Remarks

It is generally accepted that one css/js file is better than more. However, this is not possible in the current model. We could combine all the files in one response, but that would mean that:

  • We either send all the possible css/js files at once, which means one huge file with all the code users might not need. This was the initial goal, not to globally send useless code to users. This also means that whenever we add a new component at runtime, the whole collection of js/css code must be sent back to the client, although the new code could be just a small file, and the already existing large collection of code could be pretty large.
  • Or we send one large file with all the needed components in the current page. This means that several files would be sent more than once, and just because the current document needs one small new js, the client will have to receive all the files already cached.
  • Or we send one large file with essential code, and we keep the SX model for optional extensions. In a way, this is what the proposal does, considering that the skin should only need prototype (or another library we might choose) as the essential code. On the CSS part, the core files should also be packed in one response, leaving only optional component styling outside.

Currently, there are so many files transfered exactly because a lot of custom code rarely needed is always sent. In a clean skin, there would be only a few files sent in a regular page view (probably just prototype and the panels script, on the js side, and the current skin files combined in one response).

Additionally, the current loading time is so large because we're not using caches properly. If the jsx and ssx actions correctly set the cache headers, then only the first time the user loads an XWiki page would the files be transfered. From that point on, only when requesting a page that uses an unseen extension will there be any other css/js request, and only for the new extension files, which would ideally be small.

The "one compressed & minified file" model works well when a site knows exactly the code it needs globally, which is to say a fairly constant webpage, where all the pages have the same style and functionality. If we want to make XWiki a really extensible and flexible web platform, then we cannot assume that we know what will be in a page.


 

Tags:
    

Get Connected