resourceloader: Async all the way

Page startup:
* Due to the startup module and top queue being asynchronous now,
  move client-nojs/client-js class handling to OutputPage to ensure
  there is no flashes of wrongly styled or unstyled content.

  To preserve compatibility for unsupported browsers, undo the
  class swap at runtime after the isCompatible() check.

ResourceLoader startup module:
* Load the startup module with <script async>.
* Use DOM methods instead of 'document.write' to create base module request (jquery|mediawiki).

mw.loader:
* Drop 'async' parameter from mw.loader.load().
* Remove the now-unused code paths for synchronous requests.

OutputPage:

* Drop '$loadCall' parameter from makeResourceLoaderLink().
  Asynchronous is now the default and only way to load JavaScript.
  This means the 'user' module "conditional document-write scripts"
  are now a simple "mw.loader.load( url )" call.

* Fix incorrect @return of makeResourceLoaderLink(). This returns
  an array not a string.

* Improve documentation of makeResourceLoaderLink().

* Drop '$inHead' parameter from getScriptsForBottomQueue(). No longer used.
  Compatibility with the $wgResourceLoaderExperimentalAsyncLoading
  feature is maintained. It just no longer needs to change the
  way the queue works since it's always asynchronous. The feature
  flag now only controls whether the bottom queue starts at the bottom
  or starts at the top.

* Remove jQuery.ready() optimisation.
  This was mostly there to avoid the setTimeout() loop jQuery does to detect
  dom-ready in IE6/IE7 (which we no longer serve JavaScript at all).
  And for a bug in Firefox with document.write (which is no longer used as of
  this commit).

Bug: T107399
Change-Id: Icba6d7a87b239bf127a221bc6bc432cfa71a4a72
This commit is contained in:
Timo Tijhof 2015-07-27 19:46:00 -07:00 committed by Ori Livneh
parent eade3aedf5
commit d7905627fd
9 changed files with 150 additions and 183 deletions

View file

@ -2703,7 +2703,6 @@ class OutputPage extends ContextSource {
} }
$ret .= $this->buildCssLinks() . "\n"; $ret .= $this->buildCssLinks() . "\n";
$ret .= $this->getHeadScripts() . "\n"; $ret .= $this->getHeadScripts() . "\n";
foreach ( $this->mHeadItems as $item ) { foreach ( $this->mHeadItems as $item ) {
@ -2762,18 +2761,16 @@ class OutputPage extends ContextSource {
} }
/** /**
* @todo Document * Construct neccecary html and loader preset states to load modules on a page.
*
* Use getHtmlFromLoaderLinks() to convert this array to HTML.
*
* @param array|string $modules One or more module names * @param array|string $modules One or more module names
* @param string $only ResourceLoaderModule TYPE_ class constant * @param string $only ResourceLoaderModule TYPE_ class constant
* @param array $extraQuery Array with extra query parameters to add to each * @param array $extraQuery [optional] Array with extra query parameters for the request
* request. array( param => value ). * @return array A list of HTML strings and array of client loader preset states
* @param bool $loadCall If true, output an (asynchronous) mw.loader.load()
* call rather than a "<script src='...'>" tag.
* @return string The html "<script>", "<link>" and "<style>" tags
*/ */
public function makeResourceLoaderLink( $modules, $only, array $extraQuery = array(), public function makeResourceLoaderLink( $modules, $only, array $extraQuery = array() ) {
$loadCall = false
) {
$modules = (array)$modules; $modules = (array)$modules;
$links = array( $links = array(
@ -2796,7 +2793,7 @@ class OutputPage extends ContextSource {
if ( ResourceLoader::inDebugMode() ) { if ( ResourceLoader::inDebugMode() ) {
// Recursively call us for every item // Recursively call us for every item
foreach ( $modules as $name ) { foreach ( $modules as $name ) {
$link = $this->makeResourceLoaderLink( $name, $only ); $link = $this->makeResourceLoaderLink( $name, $only, $extraQuery );
$links['html'] = array_merge( $links['html'], $link['html'] ); $links['html'] = array_merge( $links['html'], $link['html'] );
$links['states'] += $link['states']; $links['states'] += $link['states'];
} }
@ -2908,30 +2905,19 @@ class OutputPage extends ContextSource {
// Automatically select style/script elements // Automatically select style/script elements
if ( $only === ResourceLoaderModule::TYPE_STYLES ) { if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
$link = Html::linkedStyle( $url ); $link = Html::linkedStyle( $url );
} elseif ( $loadCall ) {
$link = ResourceLoader::makeInlineScript(
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
);
} else { } else {
$link = Html::linkedScript( $url ); if ( $context->getRaw() || $isRaw ) {
if ( !$context->getRaw() && !$isRaw ) { // Startup module can't load itself, needs to use <script> instead of mw.loader.load
// Wrap only=script / only=combined requests in a conditional as $link = Html::element( 'script', array(
// browsers not supported by the startup module would unconditionally // In SpecialJavaScriptTest, QUnit must load synchronous
// execute this module. Otherwise users will get "ReferenceError: mw is 'async' => !isset( $extraQuery['sync'] ),
// undefined" or "jQuery is undefined" from e.g. a "site" module. 'src' => $url
) );
} else {
$link = ResourceLoader::makeInlineScript( $link = ResourceLoader::makeInlineScript(
Xml::encodeJsCall( 'document.write', array( $link ) ) Xml::encodeJsCall( 'mw.loader.load', array( $url ) )
); );
} }
// For modules requested directly in the html via <link> or <script>,
// tell mw.loader they are being loading to prevent duplicate requests.
foreach ( $grpModules as $key => $module ) {
// Don't output state=loading for the startup module..
if ( $key !== 'startup' ) {
$links['states'][$key] = 'loading';
}
}
} }
if ( $group == 'noscript' ) { if ( $group == 'noscript' ) {
@ -2980,8 +2966,20 @@ class OutputPage extends ContextSource {
* @return string HTML fragment * @return string HTML fragment
*/ */
function getHeadScripts() { function getHeadScripts() {
// Startup - this will immediately load jquery and mediawiki modules
$links = array(); $links = array();
// Client profile classes for <html>. Allows for easy hiding/showing of UI components.
// Must be done synchronously on every page to avoid flashes of wrong content.
// Note: This class distinguishes MediaWiki-supported JavaScript from the rest.
// The "rest" includes browsers that support JavaScript but not supported by our runtime.
// For the performance benefit of the majority, this is added unconditionally here and is
// then fixed up by the startup module for unsupported browsers.
$links[] = Html::inlineScript(
'document.documentElement.className = document.documentElement.className'
. '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
);
// Startup - this provides the client with the module manifest and loads jquery and mediawiki base modules
$links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS ); $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
// Load config before anything else // Load config before anything else
@ -3013,7 +3011,7 @@ class OutputPage extends ContextSource {
); );
if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
$links[] = $this->getScriptsForBottomQueue( true ); $links[] = $this->getScriptsForBottomQueue();
} }
return self::getHtmlFromLoaderLinks( $links ); return self::getHtmlFromLoaderLinks( $links );
@ -3026,23 +3024,21 @@ class OutputPage extends ContextSource {
* 'bottom', legacy scripts ($this->mScripts), user preferences, site JS * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS
* and user JS. * and user JS.
* *
* @param bool $inHead If true, this HTML goes into the "<head>", * @param bool $unused Previously used to let this method change its output based
* if false it goes into the "<body>". * on whether it was called by getHeadScripts() or getBottomScripts().
* @return string * @return string
*/ */
function getScriptsForBottomQueue( $inHead ) { function getScriptsForBottomQueue( $unused = null ) {
// Scripts "only" requests marked for bottom inclusion // Scripts "only" requests marked for bottom inclusion
// If we're in the <head>, use load() calls rather than <script src="..."> tags // If we're in the <head>, use load() calls rather than <script src="..."> tags
$links = array(); $links = array();
$links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), $links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
ResourceLoaderModule::TYPE_SCRIPTS, /* $extraQuery = */ array(), ResourceLoaderModule::TYPE_SCRIPTS
/* $loadCall = */ $inHead
); );
$links[] = $this->makeResourceLoaderLink( $this->getModuleStyles( true, 'bottom' ), $links[] = $this->makeResourceLoaderLink( $this->getModuleStyles( true, 'bottom' ),
ResourceLoaderModule::TYPE_STYLES, /* $extraQuery = */ array(), ResourceLoaderModule::TYPE_STYLES
/* $loadCall = */ $inHead
); );
// Modules requests - let the client calculate dependencies and batch requests as it likes // Modules requests - let the client calculate dependencies and batch requests as it likes
@ -3050,7 +3046,7 @@ class OutputPage extends ContextSource {
$modules = $this->getModules( true, 'bottom' ); $modules = $this->getModules( true, 'bottom' );
if ( $modules ) { if ( $modules ) {
$links[] = ResourceLoader::makeInlineScript( $links[] = ResourceLoader::makeInlineScript(
Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) ) Xml::encodeJsCall( 'mw.loader.load', array( $modules ) )
); );
} }
@ -3069,7 +3065,7 @@ class OutputPage extends ContextSource {
// We're on a preview of a JS subpage. Exclude this page from the user module (T28283) // We're on a preview of a JS subpage. Exclude this page from the user module (T28283)
// and include the draft contents as a raw script instead. // and include the draft contents as a raw script instead.
$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED, $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
); );
// Load the previewed JS // Load the previewed JS
$links[] = ResourceLoader::makeInlineScript( $links[] = ResourceLoader::makeInlineScript(
@ -3093,15 +3089,11 @@ class OutputPage extends ContextSource {
// the excluded subpage. // the excluded subpage.
} else { } else {
// Include the user module normally, i.e., raw to avoid it being wrapped in a closure. // Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED, $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
/* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
} }
// Group JS is only enabled if site JS is enabled. // Group JS is only enabled if site JS is enabled.
$links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED, $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED );
/* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
return self::getHtmlFromLoaderLinks( $links ); return self::getHtmlFromLoaderLinks( $links );
} }
@ -3114,17 +3106,11 @@ class OutputPage extends ContextSource {
// In case the skin wants to add bottom CSS // In case the skin wants to add bottom CSS
$this->getSkin()->setupSkinUserCss( $this ); $this->getSkin()->setupSkinUserCss( $this );
// Optimise jQuery ready event cross-browser. if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
// This also enforces $.isReady to be true at </body> which fixes the // Already handled by getHeadScripts()
// mw.loader bug in Firefox with using document.write between </body> return '';
// and the DOMContentReady event (bug 47457).
$html = Html::inlineScript( 'if(window.jQuery)jQuery.ready();' );
if ( !$this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) {
$html .= $this->getScriptsForBottomQueue( false );
} }
return $this->getScriptsForBottomQueue();
return $html;
} }
/** /**

View file

@ -335,7 +335,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
}, array( }, array(
'$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
'$VARS.configuration' => $this->getConfigSettings( $context ), '$VARS.configuration' => $this->getConfigSettings( $context ),
'$VARS.baseModulesScript' => Html::linkedScript( self::getStartupModulesUrl( $context ) ), '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
) ); ) );
$pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) ); $pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) );

View file

@ -210,8 +210,21 @@ HTML;
$query['only'] = 'scripts'; $query['only'] = 'scripts';
$startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) ); $startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) );
$query['raw'] = true;
$modules = $rl->getTestModuleNames( 'qunit' ); $modules = $rl->getTestModuleNames( 'qunit' );
// Disable autostart because we load modules asynchronously. By default, QUnit would start
// at domready when there are no tests loaded and also fire 'QUnit.done' which then instructs
// Karma to end the run before the tests even started.
$qunitConfig = 'QUnit.config.autostart = false;'
. 'if (window.__karma__) {'
// karma-qunit's use of autostart=false and QUnit.start conflicts with ours.
// Hack around this by replacing 'karma.loaded' with a no-op and call it ourselves later.
// See <https://github.com/karma-runner/karma-qunit/issues/27>.
. 'window.__karma__.loaded = function () {};'
. '}';
// The below is essentially a pure-javascript version of OutputPage::getHeadScripts. // The below is essentially a pure-javascript version of OutputPage::getHeadScripts.
$startup = $rl->makeModuleResponse( $startupContext, array( $startup = $rl->makeModuleResponse( $startupContext, array(
'startup' => $rl->getModule( 'startup' ), 'startup' => $rl->getModule( 'startup' ),
@ -225,35 +238,39 @@ HTML;
'user.options' => $rl->getModule( 'user.options' ), 'user.options' => $rl->getModule( 'user.options' ),
'user.tokens' => $rl->getModule( 'user.tokens' ), 'user.tokens' => $rl->getModule( 'user.tokens' ),
) ); ) );
$code .= Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ); // Catch exceptions (such as "dependency missing" or "unknown module") so that we
// always start QUnit. Re-throw so that they are caught and reported as global exceptions
// by QUnit and Karma.
$code .= '(function () {'
. 'var start = window.__karma__ ? window.__karma__.start : QUnit.start;'
. 'try {'
. 'mw.loader.using( ' . Xml::encodeJsVar( $modules ) . ' ).always( start );'
. '} catch ( e ) { start(); throw e; }'
. '}());';
header( 'Content-Type: text/javascript; charset=utf-8' ); header( 'Content-Type: text/javascript; charset=utf-8' );
header( 'Cache-Control: private, no-cache, must-revalidate' ); header( 'Cache-Control: private, no-cache, must-revalidate' );
header( 'Pragma: no-cache' ); header( 'Pragma: no-cache' );
echo $qunitConfig;
echo $startup; echo $startup;
echo "\n"; // The following has to be deferred via RLQ because the startup module is asynchronous.
// Note: The following has to be wrapped in a script tag because the startup module also echo ResourceLoader::makeLoaderConditionalScript( $code );
// writes a script tag (the one loading mediawiki.js). Script tags are synchronous, block
// each other, and run in order. But they don't nest. The code appended after the startup
// module runs before the added script tag is parsed and executed.
echo Xml::encodeJsCall( 'document.write', array( Html::inlineScript( $code ) ) );
} }
private function plainQUnit() { private function plainQUnit() {
$out = $this->getOutput(); $out = $this->getOutput();
$out->disable(); $out->disable();
$url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
) );
$styles = $out->makeResourceLoaderLink( 'jquery.qunit', $styles = $out->makeResourceLoaderLink( 'jquery.qunit',
ResourceLoaderModule::TYPE_STYLES ResourceLoaderModule::TYPE_STYLES
); );
// Use 'raw' since this is a plain HTML page without ResourceLoader
// Use 'raw' because QUnit loads before ResourceLoader initialises (omit mw.loader.state call)
// Use 'test' to ensure OutputPage doesn't use the "async" attribute because QUnit must
// load before qunit/export.
$scripts = $out->makeResourceLoaderLink( 'jquery.qunit', $scripts = $out->makeResourceLoaderLink( 'jquery.qunit',
ResourceLoaderModule::TYPE_SCRIPTS, ResourceLoaderModule::TYPE_SCRIPTS,
array( 'raw' => 'true' ) array( 'raw' => true, 'sync' => true )
); );
$head = implode( "\n", array_merge( $styles['html'], $scripts['html'] ) ); $head = implode( "\n", array_merge( $styles['html'], $scripts['html'] ) );
@ -265,6 +282,10 @@ $head
$summary $summary
<div id="qunit"></div> <div id="qunit"></div>
HTML; HTML;
$url = $this->getPageTitle( 'qunit/export' )->getFullURL( array(
'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false',
) );
$html .= "\n" . Html::linkedScript( $url ); $html .= "\n" . Html::linkedScript( $url );
header( 'Content-Type: text/html; charset=utf-8' ); header( 'Content-Type: text/html; charset=utf-8' );

View file

@ -17,7 +17,7 @@
var $spinner, href, rcid, apiRequest; var $spinner, href, rcid, apiRequest;
// Start preloading the notification module (normally loaded by mw.notify()) // Start preloading the notification module (normally loaded by mw.notify())
mw.loader.load( ['mediawiki.notification'], null, true ); mw.loader.load( 'mediawiki.notification' );
// Hide the link and create a spinner to show it inside the brackets. // Hide the link and create a spinner to show it inside the brackets.
$spinner = $.createSpinner( { $spinner = $.createSpinner( {

View file

@ -1,12 +1,11 @@
( function ( mw, $ ) { ( function ( mw, $ ) {
mw.page = {}; // Support: MediaWiki < 1.26
// Cached HTML will not yet have this from OutputPage::getHeadScripts.
document.documentElement.className = document.documentElement.className
.replace( /(^|\s)client-nojs(\s|$)/, '$1client-js$2' );
// Client profile classes for <html> mw.page = {};
// Allows for easy hiding/showing of JS or no-JS-specific UI elements
$( document.documentElement )
.addClass( 'client-js' )
.removeClass( 'client-nojs' );
$( function () { $( function () {
mw.util.init(); mw.util.init();

View file

@ -116,7 +116,7 @@
var action, api, $link; var action, api, $link;
// Start preloading the notification module (normally loaded by mw.notify()) // Start preloading the notification module (normally loaded by mw.notify())
mw.loader.load( ['mediawiki.notification'], null, true ); mw.loader.load( 'mediawiki.notification' );
action = mwUriGetAction( this.href ); action = mwUriGetAction( this.href );

View file

@ -1111,39 +1111,24 @@
} }
/** /**
* Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, * Load and execute a script with callback.
* depending on whether document-ready has occurred yet and whether we are in async mode.
* *
* @private * @private
* @param {string} src URL to script, will be used as the src attribute in the script tag * @param {string} src URL to script, will be used as the src attribute in the script tag
* @param {Function} [callback] Callback which will be run when the script is done * @param {Function} [callback] Callback which will be run when the script is done
* @param {boolean} [async=false] Whether to load modules asynchronously.
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/ */
function addScript( src, callback, async ) { function addScript( src, callback ) {
// Using isReady directly instead of storing it locally from a $().ready callback (bug 31895) $.ajax( {
if ( $.isReady || async ) { url: src,
$.ajax( { dataType: 'script',
url: src, // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
dataType: 'script', // XHR for a same domain request instead of <script>, which changes the request
// Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use // headers (potentially missing a cache hit), and reduces caching in general
// XHR for a same domain request instead of <script>, which changes the request // since browsers cache XHR much less (if at all). And XHR means we retreive
// headers (potentially missing a cache hit), and reduces caching in general // text, so we'd need to $.globalEval, which then messes up line numbers.
// since browsers cache XHR much less (if at all). And XHR means we retreive crossDomain: true,
// text, so we'd need to $.globalEval, which then messes up line numbers. cache: true
crossDomain: true, } ).always( callback );
cache: true,
async: true
} ).always( callback );
} else {
/*jshint evil:true */
document.write( mw.html.element( 'script', { 'src': src }, '' ) );
if ( callback ) {
// Document.write is synchronous, so this is called when it's done.
// FIXME: That's a lie. doc.write isn't actually synchronous.
callback();
}
}
} }
/** /**
@ -1196,7 +1181,7 @@
registry[module].state = 'ready'; registry[module].state = 'ready';
handlePending( module ); handlePending( module );
}; };
nestedAddScript = function ( arr, callback, async, i ) { nestedAddScript = function ( arr, callback, i ) {
// Recursively call addScript() in its own callback // Recursively call addScript() in its own callback
// for each element of arr. // for each element of arr.
if ( i >= arr.length ) { if ( i >= arr.length ) {
@ -1206,12 +1191,12 @@
} }
addScript( arr[i], function () { addScript( arr[i], function () {
nestedAddScript( arr, callback, async, i + 1 ); nestedAddScript( arr, callback, i + 1 );
}, async ); } );
}; };
if ( $.isArray( script ) ) { if ( $.isArray( script ) ) {
nestedAddScript( script, markModuleReady, registry[module].async, 0 ); nestedAddScript( script, markModuleReady, 0 );
} else if ( $.isFunction( script ) ) { } else if ( $.isFunction( script ) ) {
// Pass jQuery twice so that the signature of the closure which wraps // Pass jQuery twice so that the signature of the closure which wraps
// the script can bind both '$' and 'jQuery'. // the script can bind both '$' and 'jQuery'.
@ -1261,37 +1246,29 @@
mw.templates.set( module, registry[module].templates ); mw.templates.set( module, registry[module].templates );
} }
if ( $.isReady || registry[module].async ) { // Make sure we don't run the scripts until all stylesheet insertions have completed.
// Make sure we don't run the scripts until all (potentially asynchronous) ( function () {
// stylesheet insertions have completed. var pending = 0;
( function () { checkCssHandles = function () {
var pending = 0; // cssHandlesRegistered ensures we don't take off too soon, e.g. when
checkCssHandles = function () { // one of the cssHandles is fired while we're still creating more handles.
// cssHandlesRegistered ensures we don't take off too soon, e.g. when if ( cssHandlesRegistered && pending === 0 && runScript ) {
// one of the cssHandles is fired while we're still creating more handles. runScript();
if ( cssHandlesRegistered && pending === 0 && runScript ) { runScript = undefined; // Revoke
runScript(); }
runScript = undefined; // Revoke };
cssHandle = function () {
var check = checkCssHandles;
pending++;
return function () {
if ( check ) {
pending--;
check();
check = undefined; // Revoke
} }
}; };
cssHandle = function () { };
var check = checkCssHandles; }() );
pending++;
return function () {
if ( check ) {
pending--;
check();
check = undefined; // Revoke
}
};
};
}() );
} else {
// We are in blocking mode, and so we can't afford to wait for CSS
cssHandle = function () {};
// Run immediately
checkCssHandles = runScript;
}
// Process styles (see also mw.loader.implement) // Process styles (see also mw.loader.implement)
// * back-compat: { <media>: css } // * back-compat: { <media>: css }
@ -1358,10 +1335,8 @@
* @param {string|string[]} dependencies Module name or array of string module names * @param {string|string[]} dependencies Module name or array of string module names
* @param {Function} [ready] Callback to execute when all dependencies are ready * @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute when any dependency fails * @param {Function} [error] Callback to execute when any dependency fails
* @param {boolean} [async=false] Whether to load modules asynchronously.
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/ */
function request( dependencies, ready, error, async ) { function request( dependencies, ready, error ) {
// Allow calling by single module name // Allow calling by single module name
if ( typeof dependencies === 'string' ) { if ( typeof dependencies === 'string' ) {
dependencies = [dependencies]; dependencies = [dependencies];
@ -1392,9 +1367,6 @@
return; return;
} }
queue.push( module ); queue.push( module );
if ( async ) {
registry[module].async = true;
}
} }
} ); } );
@ -1435,25 +1407,22 @@
} }
/** /**
* Asynchronously append a script tag to the end of the body * Load modules from load.php
* that invokes load.php
* @private * @private
* @param {Object} moduleMap Module map, see #buildModulesString * @param {Object} moduleMap Module map, see #buildModulesString
* @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
* @param {string} sourceLoadScript URL of load.php * @param {string} sourceLoadScript URL of load.php
* @param {boolean} async Whether to load modules asynchronously.
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/ */
function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
var request = $.extend( var request = $.extend(
{ modules: buildModulesString( moduleMap ) }, { modules: buildModulesString( moduleMap ) },
currReqBase currReqBase
); );
request = sortQuery( request ); request = sortQuery( request );
// Support: IE6 // Support: IE6
// Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script // Append &* to satisfy load.php's WebRequest::checkUrlExtension test.
// isn't actually used in IE6, but MediaWiki enforces it in general. // This script isn't actually used in IE6, but MediaWiki enforces it in general.
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async ); addScript( sourceLoadScript + '?' + $.param( request ) + '&*' );
} }
/** /**
@ -1502,7 +1471,7 @@
var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
source, concatSource, origBatch, group, i, modules, sourceLoadScript, source, concatSource, origBatch, group, i, modules, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, l, currReqBase, currReqBaseLength, moduleMap, l,
lastDotIndex, prefix, suffix, bytesAdded, async; lastDotIndex, prefix, suffix, bytesAdded;
// Build a list of request parameters common to all requests. // Build a list of request parameters common to all requests.
reqBase = { reqBase = {
@ -1615,7 +1584,6 @@
currReqBase.user = mw.config.get( 'wgUserName' ); currReqBase.user = mw.config.get( 'wgUserName' );
} }
currReqBaseLength = $.param( currReqBase ).length; currReqBaseLength = $.param( currReqBase ).length;
async = true;
// We may need to split up the request to honor the query string length limit, // We may need to split up the request to honor the query string length limit,
// so build it piece by piece. // so build it piece by piece.
l = currReqBaseLength + 9; // '&modules='.length == 9 l = currReqBaseLength + 9; // '&modules='.length == 9
@ -1639,9 +1607,8 @@
if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) { if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
// This request would become too long, create a new one // This request would become too long, create a new one
// and fire off the old one // and fire off the old one
doRequest( moduleMap, currReqBase, sourceLoadScript, async ); doRequest( moduleMap, currReqBase, sourceLoadScript );
moduleMap = {}; moduleMap = {};
async = true;
l = currReqBaseLength + 9; l = currReqBaseLength + 9;
mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } ); mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
} }
@ -1649,17 +1616,11 @@
moduleMap[prefix] = []; moduleMap[prefix] = [];
} }
moduleMap[prefix].push( suffix ); moduleMap[prefix].push( suffix );
if ( !registry[modules[i]].async ) {
// If this module is blocking, make the entire request blocking
// This is slightly suboptimal, but in practice mixing of blocking
// and async modules will only occur in debug mode.
async = false;
}
l += bytesAdded; l += bytesAdded;
} }
// If there's anything left in moduleMap, request that too // If there's anything left in moduleMap, request that too
if ( !$.isEmptyObject( moduleMap ) ) { if ( !$.isEmptyObject( moduleMap ) ) {
doRequest( moduleMap, currReqBase, sourceLoadScript, async ); doRequest( moduleMap, currReqBase, sourceLoadScript );
} }
} }
} }
@ -1888,11 +1849,8 @@
* @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
* external script or style; acceptable values are "text/css" and * external script or style; acceptable values are "text/css" and
* "text/javascript"; if no type is provided, text/javascript is assumed. * "text/javascript"; if no type is provided, text/javascript is assumed.
* @param {boolean} [async] Whether to load modules asynchronously.
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
* Defaults to `true` if loading a URL, `false` otherwise.
*/ */
load: function ( modules, type, async ) { load: function ( modules, type ) {
var filtered, l; var filtered, l;
// Validate input // Validate input
@ -1902,10 +1860,6 @@
// Allow calling with an external url or single dependency as a string // Allow calling with an external url or single dependency as a string
if ( typeof modules === 'string' ) { if ( typeof modules === 'string' ) {
if ( /^(https?:)?\/\//.test( modules ) ) { if ( /^(https?:)?\/\//.test( modules ) ) {
if ( async === undefined ) {
// Assume async for bug 34542
async = true;
}
if ( type === 'text/css' ) { if ( type === 'text/css' ) {
// Support: IE 7-8 // Support: IE 7-8
// Use properties instead of attributes as IE throws security // Use properties instead of attributes as IE throws security
@ -1918,7 +1872,7 @@
return; return;
} }
if ( type === 'text/javascript' || type === undefined ) { if ( type === 'text/javascript' || type === undefined ) {
addScript( modules, null, async ); addScript( modules );
return; return;
} }
// Unknown type // Unknown type
@ -1948,7 +1902,7 @@
return; return;
} }
// Since some modules are not yet ready, queue up a request. // Since some modules are not yet ready, queue up a request.
request( filtered, undefined, undefined, async ); request( filtered, undefined, undefined );
}, },
/** /**

View file

@ -93,5 +93,14 @@ function startUp() {
// Conditional script injection // Conditional script injection
if ( isCompatible() ) { if ( isCompatible() ) {
document.write( $VARS.baseModulesScript ); ( function () {
var script = document.createElement( 'script' );
script.src = $VARS.baseModulesUri;
document.getElementsByTagName( 'head' )[0].appendChild( script );
}() );
} else {
// Undo class swapping in case of an unsupported browser.
// See OutputPage::getHeadScripts().
document.documentElement.className = document.documentElement.className
.replace( /(^|\s)client-js(\s|$)/, '$1client-nojs$2' );
} }

View file

@ -142,15 +142,13 @@ class OutputPageTest extends MediaWikiTestCase {
array( array(
array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ), array( 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ),
"<script>var RLQ = RLQ || []; RLQ.push( function () {\n" "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
. 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?' . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback\u0026*");'
. 'debug=false\u0026amp;lang=en\u0026amp;modules=test.foo\u0026amp;only'
. '=scripts\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");'
. "\n} );</script>" . "\n} );</script>"
), ),
array( array(
// Don't condition wrap raw modules (like the startup module) // Don't condition wrap raw modules (like the startup module)
array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ), array( 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ),
'<script src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></script>' '<script async src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback&amp;*"></script>'
), ),
// Load module styles only // Load module styles only
// This also tests the order the modules are put into the url // This also tests the order the modules are put into the url
@ -188,10 +186,10 @@ class OutputPageTest extends MediaWikiTestCase {
array( array(
array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ), array( array( 'test.group.foo', 'test.group.bar' ), ResourceLoaderModule::TYPE_COMBINED ),
"<script>var RLQ = RLQ || []; RLQ.push( function () {\n" "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
. 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.bar\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");' . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.bar\u0026skin=fallback\u0026*");'
. "\n} );</script>\n" . "\n} );</script>\n"
. "<script>var RLQ = RLQ || []; RLQ.push( function () {\n" . "<script>var RLQ = RLQ || []; RLQ.push( function () {\n"
. 'document.write("\u003Cscript src=\"http://127.0.0.1:8080/w/load.php?debug=false\u0026amp;lang=en\u0026amp;modules=test.group.foo\u0026amp;skin=fallback\u0026amp;*\"\u003E\u003C/script\u003E");' . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.foo\u0026skin=fallback\u0026*");'
. "\n} );</script>" . "\n} );</script>"
), ),
); );