ResourceLoader/Migration guide for extension developers

From mediawiki.org

Target system[edit]

You might encounter the following server-side warning in your logs:

Module "{module}" not loadable on target "{target}".

Or this client-side console warning:

Skipped unresolvable module {module}.

This most likely it means a module is defined with a mobile target, but unconditionally enqueued on pages on desktop as well. Using targets in this way is deprecated per T127268 and should be avoided in new code.

Paths forward:

  • Consider whether the code could easily be loaded on desktop without harm (e.g. performance, broken functionality, or duplicate functionality).
  • If the module is a mobile-friendly version of another module, evaluate whether this is truly about form factor and device capabilities versus the layout of the Minerva skin. In general, it is recommended that you combine such features into one module and use skinScripts and skinStyles for aspects that relate to the skin, and code for narrow viewports or touch events should be run unconditionally regardless of skin or device as anyone may use a narrow window on a large device, or have large devices with touch event support. Set the targets to both mobile, desktop in this case.
  • If the feature exists only for Minerva and has no equivalent for other skins or desktop, move the resources to skinScripts/skinStyles, and set the targets to both mobile, desktop in this case.
  • If the script bundle is significant in size and the feature is only necessary on mobile browsers but not skin, and dependent on something that can be feature detected on the client, e.g. browser viewport size or touchevents, its best to break out a small module for running conditional loads on the client like so:
    var isTouchDevice = 'ontouchstart' in document.documentElement;
    if ( isTouchDevice ) {
        mw.loader.load('ext.touchEventEnhancement');
    }
    if ( window.innerWidth < 400 ) {
        mw.loader.load('ext.mobileEnhancement');
    }
    
  • If the feature requires a specific extension e.g. Echo, you should conditionally load it based on Echo on the server side, like so:
    $services = MediaWikiServices::getInstance();
    $isEchoInstalled = ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) &&
    if ( $isEchoInstalled ) {
    		$out->addModules( 'ext.foo.bar' );
    }
    
  • To conditionally lazy-load something client-side only based on the existence of a module, add something like the below code to your general "init" module. This init module should be shared by your extension as a whole, and added server-side to any page where a lazy-loaded thing may be needed. Both the init module and the lazy-loaded module should be defined with target=mobile,desktop. Remember that it is okay for multiple special pages or hooks to queue the same module name. ResourceLoader will not download or execute any module more than once.
    function onMyLazyCommand() {
      var moduleState = mw.loader.getState( 'mobile.startup' );
      if ( stars.areAligned() && moduleState && moduleState !== 'registered' ) {
        mw.loader.load( 'ext.cx.languagesearcher' );
      }
    }
    

In all of the above options, the outcome for the targets property is the same: Set it to both mobile, desktop which reflects what will happen when that property is ignored / no longer supported.

Note, that historically certain extensions have been designed to run only in the mobile domain. These should ideally be rewritten using one of the bullet points above, but as a last resort, the following pattern can be used to check whether MobileFrontend is enabled and a page view is inside the mobile domain:

if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) {
	$mobileContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
	$isMobileView = $mobileContext->shouldDisplayMobileView();
	if ( $isMobileView ) {
	    $out->addModule( 'ext.foo.bar' );
	}
}

This is generally discouraged and should only be used in exceptional circumstances, if any.

Crash-course: How to use ResourceLoader[edit]

Most code written prior to MediaWiki 1.17, the introduction of ResourceLoader, should continue to work, there are some issues that are specific to the architecture of the system which may need to be resolved.

Review resources/Resources.php for more examples of module registration and includes/OutputPage.php for more examples of how to add modules to a page.

For now let's start with how to register a module:

Registering a module[edit]

To make your resources available to the loader, you will need to register them as a module, telling ResourceLoader what you want to make available and where the files are. Check out ResourceLoader/Developing with ResourceLoader#Registering to learn how to do that.

Internationalization[edit]

You can access messages specified in your resource module using the mw.message() method. This returns a Message object whose output formats can be specified as described here. Example:

alert( mw.message( 'myextension-hello-world' ).plain() );

mw.message() can accept multiple arguments, with additional arguments being passed to the message function named in the first argument (exactly in the same way as with server-side message functions):

alert( mw.message( 'myextension-about-me', myName, myTitle ).plain() );

Inline JavaScript[edit]

Before ResourceLoader was developed, nearly all JavaScript resources were added in the head of the document. With the introduction of ResourceLoader, JavaScript resources are now loaded at the bottom of the body.

The motivation for this change had to do with the fact that browsers block rendering while a script resource is downloaded and executed (this is because a script can do almost anything to the page). By placing scripts at the bottom, the entire page can be loaded before any scripts are executed, ensuring that all stylesheets and images referenced by the HTML content as well as the CSS can be queued before the browser pauses to execute scripts, thus increasing parallelism, and improving client-side performance. This also ensures that readers can start reading the page contents as soon as possible.

A side-effect of this change is that scripts that are injected arbitrarily into the different parts of the body cannot depend on functionality provided by regular scripts (which are loaded at the bottom). For this reason, it is recommended to do JavaScript bindings from the modules rather than inline.

From MediaWiki 1.18 to 1.28, there were two load queues in ResourceLoader, symbolically named "top" and "bottom". Modules could set their "position" property (in $wgResourceModules ) to one of these to force loading in either queue. The mediawiki and jquery modules were loaded from the top by default, so that they could always be used. The queues and positions were removed in MediaWiki 1.29 (phab:T109837), and everything is now at the "top".

Configuration variables[edit]

If you used to insert javascript like:

var wgFoo = "bar";

You can now use resource loader to add your variable as a config variable. If the variable is the same for all pages and should be on all pages, use the ResourceLoaderGetConfigVars hook. Otherwise if the variable depends on the current page being viewed you can either use the MakeGlobalVariablesScript hook or if you have an OutputPage object, the addJsConfigVars method. Variables added can be accessed in javascript using mw.config.get( 'variable name' );

Troubleshooting and tips[edit]

Global scope[edit]

As the scripts are loaded at the bottom of HTML, make sure that all OutputPage-generated HTML tags are properly closed. If you are porting JavaScript code to ResourceLoader, make the default outer scope variable declarations explicitly use window as their parent, because the default scope in ResourceLoader is not global (e.g. not a window). Which means that code that previously became global by using var something in a .js file, is now local. It will only be global if you make it global, by literally attaching it to the window object with window.myProperty.

An even better approach (instead of attaching vars to window) is to convert the existing extension's JavaScript code to an object oriented structure. (or, if the code is mainly about manipulating DOM elements, make a jQuery plugin).

Suppose your current structure looks like this:

var varname = ..;

function fn( a, b ){
}

In order to keep it working with existing implied globals, replace implied globals with real globals (which it should already be):

window.varname = ..;

window.fn = function( a, b ) {
}

To actually port it to an object oriented structure, you'd write something like this:

mw.myExtension = {
  varname: ...,
  fn: function( a, b ) {
  }
}

Special case of external libraries[edit]

If you are using an external JavaScript library, which is primarily maintained outside of MediaWiki extension repository, patching as suggested above is undesirable. Instead, you should create an extra JavaScript file where the required variables and functions would be bound via the references to window. Then, when registering ResourceLoaderFileModule you should pass both scripts in one PHP array:

'scripts' => array( 'ext.myext.externalCode.js', 'ext.myext.externalCode.binding.js' )

This will weld the two scripts together before throwing them into a closure.

ext.myext.externalCode.binding.js should contain assignment statements like this:

window.libvar = libvar;
window.libfunc = libfunc;

Some libraries autodetect their environment (AMD, node.js, browser, etc.) and can be mistaken in their choice because of the special environment of ResourceLoader. For instance, d3.v3.js (D3.js version 3) selects a node.js-like environment and exports its main variable d3 in module.exports (the issue doesn’t appear for d3.v4.js); in this case, d3.v3.js must have a binding file d3.v3.binding.js:

window.d3 = module.exports;

See also