ResourceLoader/Developing with ResourceLoader

shortcut: RL/DEV
From mediawiki.org

This is a high-level walkthrough of how create and load module bundles of CSS and JavaScript files, with ResourceLoader in MediaWiki.

The page loading process with ResourceLoader.

Introduction[edit]

On every page view, the JavaScript runtime starts with a registry of which bundles exist in the system, the relationships between them, and a hash of their current version. MediaWiki can be instructed to load one or more of these on a given page. These instructions take the form of an addModules() or addModuleStyles() function call in PHP. Typically from a special page, parser function, or OutputPage hook.

A bundle should generally represent all code needed together in the same end-user scenario. For example, the user-interface of the WikiEditor extension is 1 module bundle, loaded from the edit page (possibly conditional based on content type and user preferences).

Note that bundle generation is allowed to vary by site, skin, and user language. As such, the bundle can also include site-specific configuration variables, additional scripts and styles specific to one or more skins or languages, and additional dependencies or script files based on the configuration of the wiki and/or other installed extensions.

There may be small portions of the interface that are only used later or in some cases, but the general default should be to still bundle them together. Splitting modules should be reserved as an optimisation and coincide with measuring of the perceived performance problem and quantified with end-user benefit.

Registering many modules is highly discouraged, even if they are not loaded by default. The addition of each module adds 44 B[1] to every initial page load, so about 40 GiB of extra transfer to Wikimedia servers per day. If you find yourself needing more than three modules for a single extension or core feature aiming to be deployed on Wikimedia Foundation wikis, reach out to MediaWiki Platform Team who may be able to help find or design alternate approaches to solve the problem at hand. See also Page load performance § Size of scripts.

Registering[edit]

Each module bundle needs a symbolic name that is unique on a given site.

For MediaWiki core, modules are defined in the /resources/Resources.php file.

For extensions, modules are defined via the ResourceModules attribute in your /extension.json file.

Below is an example of a module definition for an extension (see also: Extension registration ).

{
    "ext.myExtension": {
        "localBasePath": "modules/ext.MyExtension",
        "remoteExtPath": "MyExtension/modules/ext.MyExtension",
        "packageFiles": [
            "index.js",
            "core.js",
            "foobar.js"
        ],
        "styles": "myExtension.css",
        "messages": [
            "myextension-hello-world",
            "myextension-goodbye-world"
        ],
        "dependencies": [
            "oojs",
            "mediawiki.util"
        ]
    }
}

For newer extensions we recommend making use of packageFiles rather than scripts.

File organization[edit]

Typically modules are kept in a folder or filename which matches the ResourceLoader module name inside a resources folder, however note older extensions or skins may use a modules folder.

For example a module called ext.foo would list assets inside a folder called resources/ext.foo/

It is recommended that files in these folders do not reference files outside the folder. For example a file index.less inside folder resources/ext.foo.foo should not reference a file inside folder resources/ext.foo.bar. If this is happening please consider creating a new ResourceLoader module for shared resources and listing it as one of the modules dependencies.

See foreign resources for conventions for copying files from upstream libraries.

Naming[edit]

It is conventional for extension bundles to carry the "ext." prefix, and skin bundles should be prefixed with "skins.". This is then followed by the extension or skin's name in lowerCamelCase form. Any additional segments should be separated by a dot.

Module bundles that are styles-only should be suffixed with ".styles", to help remind that any server-side queuing should use addModuleStyles() instead of addModules().

Definition[edit]

For more about the keys of the module definition (such as scripts, styles, messages, etc) see ResourceModules.

Gadgets and user scripts[edit]

Every gadget automatically becomes a ResourceLoader. The dependencies of a Gadget module can be defined via the "dependencies" options in their definition.

For user scripts, the only way to load dependencies is to do so lazily, by wrapping the code in a mw.loader.using block, and specify the required modules. For example:

mw.loader.using( [ 'mediawiki.util', 'mediawiki.Title' ] ).then( function () {
    // This code is executed after the above modules are loaded.
} );

Base environment[edit]

The base environment of ResourceLoader ensures the following as of MediaWiki 1.35:

  • ECMAScript 5, HTML 5, Selectors API (e.g. ES5 strict mode, Local Storage, querySelector; ensured by startup.js).
  • jQuery (e.g. $.ajax, $.ready, etc.; see also api.jquery.com).
  • mediawiki.base (e.g. mw.config, mw.loader, etc.).

If one or more of these is not supported in a browser, then ResourceLoader will automatically ensure no JavaScript is loaded or executed. As such you can safely use ES5 syntax without worrying about causing errors in older browsers. See Compatibility#Browsers for more information about browser support.



Modules that use ES6 syntax must set "es6": true in their module definition. ResourceLoader prevents ES6 modules from being loaded in browsers that don't support ES6 (such as Internet Explorer 11). ES6 modules should only be used for features where IE11 support is not required, and/or there is an acceptable no-JavaScript fallback for older browsers. ES6 should not be used in modules that are intended to be reused across different features, because ES5-supporting modules can't depend on ES6-only ones.

Loading modules[edit]

Server-side[edit]

See also Manual:OutputPage.php and ParserOutput

While building the page, add one or more modules to the page by calling the addModules method on the OutputPage or ParserOutput object and passing it one or more module names (such as "mediawiki.foo", or "ext.myExtension.quux")

$outputPage->addModules( [ 'example.fooBar' ] );

OutputPage adds the given module names to the load queue of the page. The client side loader requests all of the components for this module (scripts, styles, messages, dependencies, etc.) and executes them correctly. If your module contains styles that affect elements outputted by PHP as well as elements created dynamically, then you should split the module. One for styling/enhancing the output, and one for dynamic stuff. The first module should only have stylesheets and be loaded with addModuleStyles (see #CSS). The other module will simply be loaded asynchronously by the client, not blocking further parsing of the page.

$outputPage->addModules( [ 'mediawiki.foo', 'ext.myExtension' ] );

To get a reference to OutputPage object from an extension, use the BeforePageDisplay hook.

Style modules[edit]

If you have CSS that should be loaded before the page renders (and when JavaScript is unavailable), queue it via OutputPage::addModuleStyles. This will make sure the module is loaded from a <link rel=stylesheet> tag.

For styles relating to anything involving JavaScript, the preferred method is to include them with OutputPage::addModules which loads modules as a complete package in a combined request (scripts, styles, messages) dynamically (from a lightweight client in JavaScript). This is because it is more efficient for this purpose (single request for all resources), supports dependency resolution, request batching, is highly cacheable (through access to the startup manifest with all version numbers it can dynamically generate permanently cacheable urls), and reduces cache fragmentation (modules previously loaded on other page views are loaded directly from Local Storage). It should not be used for CSS-only modules.

Since dependency changes can be deployed independently from caching the page, static loading with addModuleStyles cannot use dependencies. And since you can't dynamically access the latest version of the startup manifest from static HTML without JavaScript execution, it cannot have versions in the urls either and are therefore cached for a shorter time.

In practice you should only use OutputPage::addModuleStyles for stylesheets that are required for basic presentation of server-generated content (PHP output and the core Skin). Separate this CSS from modules with JavaScript and styling of dynamically-generated content.

JavaScript[edit]

JavaScript files are, like CSS files, also evaluated in the order they are defined in a scripts or packageFiles array.

In the following example, one would use the entry "packageFiles": [ "index.js", "Foo.js" ] when registering the module.

// Foo.js
var Foo = {
    sayHello: function ( $element ) {
        $element.append( '<p>Hello Module!</p>' );
    }
};
module.exports = Foo;

// index.js
var Foo = require( './Foo.js' );
$( function () {
    // This code must not be executed before the document is loaded. 
    Foo.sayHello( $( '#hello' ) );
});

The page loading this module would somewhere use $this->getOutput()->addHTML( '<div id="hello"></div>' ); to output the element.

Export data from server to client[edit]

See Package modules for how to bundle additional data on the server-side with your module. These can be configuration variables, or results of rich callbacks that generate data form anywhere else that serialise to JSON, or even generated JavaScript code.

Alternatively, you might want to lazy-load the data from the API (e.g. using the mediawiki.api library).

If you need to export information specific to the current page output, e.g. from a parser function, consider using a data- attribute and selecting it from JavaScript. Or if you need more complex data, call setJsConfigVar() on the OutputPage or ParserOutput object, and access this from client-side code using mw.config.get(). In rare cases where you can't access an OutputPage or ParserOutput object, the MakeGlobalVariablesScript hook can be used.

Client-side (dynamically)[edit]

See also: mw.loader.using() API Documentation on doc.wikimedia.org.

Conditional lazy loading[edit]

If you have a script that only needs another module in a certain scenario of the user interface, you can lazy-load those from your main "init" module. For example:

var $sortableTables = $content.find( 'table.sortable' );
if ( $sortableTables.length ) {
    mw.loader.using( 'jquery.tablesorter' ).then( function () {
        $sortableTables.tablesorter();
    } );
}

Parallel execution[edit]

If there are asynchronous tasks you need to wait for in addition to the loading of dependencies, then consider making these happen in parallel.

Use jQuery.when to track multiple separate asynchronous tasks (known as a "Promise", or a "Deferred"). Below is an example of waiting for and AJAX request, and the loading of ResourceLoader modules, and the "document ready" status:

// Slow: These are nested, one waiting before the other starts:
$( function () {
  mw.loader.using( ['mediawiki.util', 'mediawiki.Title'] ).then( function () {
    $.getJSON( 'https://api.example.org/foo' ).then( function ( fooData ) {
      $( '#example' ).attr( 'href', mw.util.getUrl( fooData.page ) );
    } );
  } );
} );

// Faster: Preloading with load()
$( function () {
  mw.loader.load( ['mediawiki.util', 'mediawiki.Title'] );
  $.getJSON( 'https://api.example.org/foo' ).then( function ( fooData ) {
      mw.loader.using( ['mediawiki.util', 'mediawiki.Title'], function () {
        $( '#example' ).attr( 'href', mw.util.getUrl( fooData.page ) );
      } );
  } );
} );

// Fastest: These three processes run in parallel
$.when(
  $.getJSON( 'https://api.example.org/foo' ),
  mw.loader.using( ['mediawiki.util', 'mediawiki.Title'] ),
  $.ready
).then( function ( fooData ) {
 // This runs when the ajax request is complete, the modules are loaded,
 // and the document is ready
 $( '#example' ).attr( 'href', mw.util.getUrl( fooData.page ) );
} );

CSS[edit]

Your styling resources can be either CSS or LESS files. When writing styles we advise you to follow our code conventions.

Media queries[edit]

You can use media queries when you define your modules, to specify when a CSS/Less file applies:

{
	"styles": {
		"always.css": { "media": "screen" },
		"print.css": { "media": "print" },
		"high-resolution.css": { "media": "screen and ( min-width: 982px )" }
	}
}

In the above example, the always.css stylesheet will always apply to all screens, the print.css stylesheet applies on print (and in the "Printable version" mode), and the high-resolution.css stylesheet applies when the viewport width is at least 982 pixels. The contents of the corresponding CSS/Less file will then be wrapped in the defined media query:

/* Output of print.css by ResourceLoader */
@media print {
	/* Contents of print.css */
}

/* Output of high-resolution.css by ResourceLoader */
@media screen and ( min-width: 982px ) {
	/* Contents of high-resolution.css */
}

Annotations[edit]

The CSS preprocessors in ResourceLoader support several annotations that you can use to optimise your stylesheets.

@embed[edit]

It is no longer generally recommended to use @embed. For guidance, refer to Frontend performance practices. To learn how this annotation works, refer to ResourceLoader/Architecture#Embedding.

See also ResourceLoaderImage, which generates raster images and multiple colored icons from a single source SVG file.

@noflip[edit]

See also ResourceLoader/Architecture#Flipping

To disable the flipping functionality for one CSS declaration or on an entire ruleset, use the @noflip annotation:

For example:

/* @noflip */ 
.mw-portlet-item { 
    float: left; 
    line-height: 1.25; 
}

/* This one flips! */ 
.mw-portlet-item { 
    margin-top: 0.5em; 
    /* ... except this one: */ 
    /* @noflip */ 
    margin-left: 0.75em; 
    font-size: 0.75em; 
    white-space: nowrap; 
}

Debugging[edit]

Toggle debug mode[edit]

ResourceLoader supports complex client-side web applications in production and development environments. As these different environments have different needs, ResourceLoader offers two distinct modes: production mode and debug mode (also known as "development") mode.

Debug mode is designed to make development as easy as possible, prioritizing the ease of identifying and resolving problems in the software over performance. Production mode makes the opposite prioritization, emphasizing performance over ease of development.

It is important to test your code in both debug and production modes. In day-to-day development, most developers will find it beneficial to use debug mode most of the time, only validating their code's functionality in production mode before committing changes.

You can enable debug mode for all page views, or for a single page request (by appending ?debug=true to the URL); see ResourceLoader/Architecture#Debug mode for details on toggling it.

Server-side exceptions[edit]

ResourceLoader catches any errors thrown during module packaging (such as an error in a module definition or a missing file) in load.php requests. It outputs this error information in a JavaScript comment at the top of its response to that request, for example:

/**
 * exception 'MWException' with message 'ResourceLoaderFileModule::readStyleFile: style file not found: 
 * Problematic modules: {"skin.blueprint.styles":"error"}
 */

You can inspect the request in the network panel in the developer tools for most browsers, or you can copy the load.php URL and load it in a new browser window. Note that the HTTP request with a failing module still returns status 200 OK, it does not return an error.

You can also output errors to a server-side log file by setting up a log file ($wgDebugLogGroups ) in $wgDebugLogGroups['resourceloader']. They aren't added to the main debug log ($wgDebugLogFile ) since logging is disabled by default for requests to load.php (bug 47960).

Client-side errors[edit]

Unreviewed

JavaScript returned by ResourceLoader is executed in the browser, and can have run-time errors. Most browsers do not display these to the user, so you should leave your browser's JavaScript console open during development to notice them.

You can use ResourceLoader's mw.loader.getState() function to check the state of a module, for example enter mw.loader.getState( 'skins.vector.js' ) in your browser's JavaScript console. If it returns:

null
The module is not known to ResourceLoader. Check for typos, and verify whether the module registered and defined correctly.
registered
ResourceLoader knows about the module, but hasn't (yet) loaded it on the current page. Check your logic for adding the module, either server-side or client-side. You can force it to load by entering mw.loader.load( 'my-module-name' ).
error
Something went wrong, either server-side or during client-side execution, with this module or one of its dependencies. Check the browser console and Network inspector for error relating to a load.php request. Alternatively, consider reloading the page in debug mode.
ready
The module loaded on the current page without errors.

Bypassing cache[edit]

When making frequent changes to code and checking them in a browser, the caching mechanisms designed to improve the performance of web-browsing can quickly become inconvenient. When developing on a system which is not making use of a reverse proxy, you only need to force your browser to bypass its cache while refreshing. This can be achieved by pressing CTRL+F5 in Internet Explorer, or holding the shift key while clicking the browser's refresh button in most other browsers.

If you are developing behind a reverse proxy, you can either change the values of $wgResourceLoaderMaxage or use ?debug=true to bypass cache.

See also