blob: decf39ac6c29782092e3cfe1332cf869585b0a4c [file] [log] [blame]
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
*
* @fileoverview This class supports the dynamic loading of compiled
* javascript modules at runtime, as descibed in the designdoc.
*
* <http://go/js_modules_design>
*
*/
goog.provide('goog.module.Loader');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dom');
/** @suppress {extraRequire} */
goog.require('goog.module');
goog.require('goog.object');
/**
* The dynamic loading functionality is defined as a class. The class
* will be used as singleton. There is, however, a two step
* initialization procedure because parameters need to be passed to
* the goog.module.Loader instance.
*
* @constructor
* @final
*/
goog.module.Loader = function() {
/**
* Map of module name/array of {symbol name, callback} pairs that are pending
* to be loaded.
* @type {Object}
* @private
*/
this.pending_ = {};
/**
* Provides associative access to each module and the symbols of each module
* that have aready been loaded (one lookup for the module, another lookup
* on the module for the symbol).
* @type {Object}
* @private
*/
this.modules_ = {};
/**
* Map of module name to module url. Used to avoid fetching the same URL
* twice by keeping track of in-flight URLs.
* Note: this allows two modules to be bundled into the same file.
* @type {Object}
* @private
*/
this.pendingModuleUrls_ = {};
/**
* The base url to load modules from. This property will be set in init().
* @type {?string}
* @private
*/
this.urlBase_ = null;
/**
* Array of modules that have been requested before init() was called.
* If require() is called before init() was called, the required
* modules can obviously not yet be loaded, because their URL is
* unknown. The modules that are requested before init() are
* therefore stored in this array, and they are loaded at init()
* time.
* @type {Array<string>}
* @private
*/
this.pendingBeforeInit_ = [];
};
goog.addSingletonGetter(goog.module.Loader);
/**
* Wrapper of goog.module.Loader.require() for use in modules.
* See method goog.module.Loader.require() for
* explanation of params.
*
* @param {string} module The name of the module. Usually, the value
* is defined as a constant whose name starts with MOD_.
* @param {number|string} symbol The ID of the symbol. Usually, the value is
* defined as a constant whose name starts with SYM_.
* @param {Function} callback This function will be called with the
* resolved symbol as the argument once the module is loaded.
*/
goog.module.Loader.require = function(module, symbol, callback) {
goog.module.Loader.getInstance().require(module, symbol, callback);
};
/**
* Wrapper of goog.module.Loader.provide() for use in modules
* See method goog.module.Loader.provide() for explanation of params.
*
* @param {string} module The name of the module. Cf. parameter module
* of method require().
* @param {number|string=} opt_symbol The symbol being defined, or nothing
* when all symbols of the module are defined. Cf. parameter symbol of
* method require().
* @param {Object=} opt_object The object bound to the symbol, or nothing when
* all symbols of the module are defined.
*/
goog.module.Loader.provide = function(module, opt_symbol, opt_object) {
goog.module.Loader.getInstance().provide(
module, opt_symbol, opt_object);
};
/**
* Wrapper of init() so that we only need to export this single
* identifier instead of three. See method goog.module.Loader.init() for
* explanation of param.
*
* @param {string} urlBase The URL of the base library.
* @param {Function=} opt_urlFunction Function that creates the URL for the
* module file. It will be passed the base URL for module files and the
* module name and should return the fully-formed URL to the module file to
* load.
*/
goog.module.Loader.init = function(urlBase, opt_urlFunction) {
goog.module.Loader.getInstance().init(urlBase, opt_urlFunction);
};
/**
* Produces a function that delegates all its arguments to a
* dynamically loaded function. This is used to export dynamically
* loaded functions.
*
* @param {string} module The module to load from.
* @param {number|string} symbol The ID of the symbol to load from the module.
* This symbol must resolve to a function.
* @return {!Function} A function that forwards all its arguments to
* the dynamically loaded function specified by module and symbol.
*/
goog.module.Loader.loaderCall = function(module, symbol) {
return function() {
var args = arguments;
goog.module.Loader.require(module, symbol, function(f) {
f.apply(null, args);
});
};
};
/**
* Creates a full URL to the compiled module code given a base URL and a
* module name. By default it's urlBase + '_' + module + '.js'.
* @param {string} urlBase URL to the module files.
* @param {string} module Module name.
* @return {string} The full url to the module binary.
* @private
*/
goog.module.Loader.prototype.getModuleUrl_ = function(urlBase, module) {
return urlBase + '_' + module + '.js';
};
/**
* The globally exported name of the load callback. Matches the
* definition in the js_modular_binary() BUILD rule.
* @type {string}
*/
goog.module.Loader.LOAD_CALLBACK = '__gjsload__';
/**
* Loads the module by evaluating the javascript text in the current
* scope. Uncompiled, base identifiers are visible in the global scope;
* when compiled they are visible in the closure of the anonymous
* namespace. Notice that this cannot be replaced by the global eval,
* because the global eval isn't in the scope of the anonymous
* namespace function that the jscompiled code lives in.
*
* @param {string} t_ The javascript text to evaluate. IMPORTANT: The
* name of the identifier is chosen so that it isn't compiled and
* hence cannot shadow compiled identifiers in the surrounding scope.
* @private
*/
goog.module.Loader.loaderEval_ = function(t_) {
eval(t_);
};
/**
* Initializes the Loader to be fully functional. Also executes load
* requests that were received before initialization. Must be called
* exactly once, with the URL of the base library. Module URLs are
* derived from the URL of the base library by inserting the module
* name, preceded by a period, before the .js prefix of the base URL.
*
* @param {string} baseUrl The URL of the base library.
* @param {Function=} opt_urlFunction Function that creates the URL for the
* module file. It will be passed the base URL for module files and the
* module name and should return the fully-formed URL to the module file to
* load.
*/
goog.module.Loader.prototype.init = function(baseUrl, opt_urlFunction) {
// For the use by the module wrappers, loaderEval_ is exported to
// the page. Note that, despite the name, this is not part of the
// API, so it is here and not in api_app.js. Cf. BUILD. Note this is
// done before the first load requests are sent.
goog.exportSymbol(goog.module.Loader.LOAD_CALLBACK,
goog.module.Loader.loaderEval_);
this.urlBase_ = baseUrl.replace(/\.js$/, '');
if (opt_urlFunction) {
this.getModuleUrl_ = opt_urlFunction;
}
goog.array.forEach(this.pendingBeforeInit_, function(module) {
this.load_(module);
}, this);
goog.array.clear(this.pendingBeforeInit_);
};
/**
* Requests the loading of a symbol from a module. When the module is
* loaded, the requested symbol will be passed as argument to the
* function callback.
*
* @param {string} module The name of the module. Usually, the value
* is defined as a constant whose name starts with MOD_.
* @param {number|string} symbol The ID of the symbol. Usually, the value is
* defined as a constant whose name starts with SYM_.
* @param {Function} callback This function will be called with the
* resolved symbol as the argument once the module is loaded.
*/
goog.module.Loader.prototype.require = function(module, symbol, callback) {
var pending = this.pending_;
var modules = this.modules_;
if (modules[module]) {
// already loaded
callback(modules[module][symbol]);
} else if (pending[module]) {
// loading is pending from another require of the same module
pending[module].push([symbol, callback]);
} else {
// not loaded, and not requested
pending[module] = [[symbol, callback]]; // Yes, really [[ ]].
// Defer loading to initialization if Loader is not yet
// initialized, otherwise load the module.
if (goog.isString(this.urlBase_)) {
this.load_(module);
} else {
this.pendingBeforeInit_.push(module);
}
}
};
/**
* Registers a symbol in a loaded module. When called without symbol,
* registers the module to be fully loaded and executes all callbacks
* from pending require() callbacks for this module.
*
* @param {string} module The name of the module. Cf. parameter module
* of method require().
* @param {number|string=} opt_symbol The symbol being defined, or nothing when
* all symbols of the module are defined. Cf. parameter symbol of method
* require().
* @param {Object=} opt_object The object bound to the symbol, or nothing when
* all symbols of the module are defined.
*/
goog.module.Loader.prototype.provide = function(
module, opt_symbol, opt_object) {
var modules = this.modules_;
var pending = this.pending_;
if (!modules[module]) {
modules[module] = {};
}
if (opt_object) {
// When an object is provided, just register it.
modules[module][opt_symbol] = opt_object;
} else if (pending[module]) {
// When no object is provided, and there are pending require()
// callbacks for this module, execute them.
for (var i = 0; i < pending[module].length; ++i) {
var symbol = pending[module][i][0];
var callback = pending[module][i][1];
callback(modules[module][symbol]);
}
delete pending[module];
delete this.pendingModuleUrls_[module];
}
};
/**
* Starts to load a module. Assumes that init() was called.
*
* @param {string} module The name of the module.
* @private
*/
goog.module.Loader.prototype.load_ = function(module) {
// NOTE(user): If the module request happens inside a click handler
// (presumably inside any user event handler, but the onload event
// handler is fine), IE will load the script but not execute
// it. Thus we break out of the current flow of control before we do
// the load. For the record, for IE it would have been enough to
// just defer the assignment to src. Safari doesn't execute the
// script if the assignment to src happens *after* the script
// element is inserted into the DOM.
goog.Timer.callOnce(function() {
// The module might have been registered in the interim (if fetched as part
// of another module fetch because they share the same url)
if (this.modules_[module]) {
return;
}
goog.asserts.assertString(this.urlBase_);
var url = this.getModuleUrl_(this.urlBase_, module);
// Check if specified URL is already in flight
var urlInFlight = goog.object.containsValue(this.pendingModuleUrls_, url);
this.pendingModuleUrls_[module] = url;
if (urlInFlight) {
return;
}
var s = goog.dom.createDom('script',
{'type': 'text/javascript', 'src': url});
document.body.appendChild(s);
}, 0, this);
};