270 lines
9.0 KiB
JavaScript
270 lines
9.0 KiB
JavaScript
// 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');
|
|
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
|
|
*/
|
|
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);
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
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);
|
|
};
|