In the
last part, we wrote a small library, pozhuzhu on require.js and allows you to load AMD-modules. It is time to expand its capabilities and make it a full replacement for the original require.js. Therefore, today we are implementing the ability to customize, similar to the
require.config()
function and plugin support, so that all additions to the usual require.js work here.
Some utilitarian features
')
Require allows us not only to load and declare modules, it also has several utility functions for our convenience.
toUrl is a fairly easy function that turns the module name into the path to the file. While we do not have support of many features require,
she looks simple:
function toUrl(name, appendJS) { return './' + name + (appendJS ? '.js' : ''); }
The ability to add an extension is used exclusively inside the library, for users this flag is always false
require.toUrl = function(name) { return toUrl(name, false); };
And we will set
true
when calling from the
loadScript
function:
el.src = toUrl(name, true);
Thus, we will provide users with the ability to build URLs according to our rules, but we will retain a monopoly for
Download js-files.
specified - check if the specified module is in the list of loaded or loaded
require.specified = function(name) { return modules.hasOwnProperty(name); };
undef - removes a module definition from the list.
require.undef = function(name) { delete modules[name]; };
onError is a standard error handler. If you do not want to see the error message in the console, you need to
override it. We bring out our error handling to this function:
if(typeof errback
There remains one very important function -
require.config
. But it is so significant that we will deal with it later.
See the code for this step on Github .
Boot settings
Not always the standard behavior will suit everyone. Require.js can be configured via the
require.config()
function. It has arrived
time and we support her. We will have a config variable in which we can write our options and use them.
in work. The function will accept new options and add them to existing ones, for example, using the
deepmerge library,
found in the npm.
var config = {}; require.config = function(options) { config = deepMerge(config, options); }
When we have a mechanism for saving settings, we need to figure out what options we need to support.
baseUrl - the base path to the modules, the path to all modules begins with it. The default is the address of the current
page, that is, its value
./
. Add its support to the
require.toUrl()
function.
toUrl = function(name, appendJS) { - return './' + name + (appendJS ? '.js' : ''); + return config.baseUrl + name + (appendJS ? '.js' : ''); }
This option is useful to us for tests, the modules are in the fixtures folder, and now we can specify it once, and not
repeat all the time.
urlArgs is a string added to the url end of the loadable modules. It is useful to save a specific version of the modules.
in the browser cache.
paths - paths for those files that are separate from other modules. Usually these are popular libraries.
for example jQuery, which are on the CDN, so it would be nice to declare that the
jquery
module should be loaded by the url of the form
//code.jquery.com/jquery-2.1.3.min.js
. Support the presence of a module in the paths in our code.
if(config.paths[name]) { name = config.paths[name]; } if(!/^([\w\+\.\-]+:)?\/.test(name)) { name = config.baseUrl + name; } if(config.urlArgs) { name += '?'+config.urlArgs; }
Making the way to the module has become more difficult.
- Take it from the paths if there is one.
- Add baseUrl if our path is not absolute
- Add GET parameters if required
Require works the same way, so these settings will give the same result as there.
bundles - a recent innovation, but useful. We can create a file with several modules (bundle), it will
load when at least one of the specified modules is called. To support this option, we need to look for a module.
in one of the packs, and if we find it, then download this file instead of the original one.
+var bundle = Object.keys(config.bundles).filter(function(bundle) { + return config.bundles[bundle].indexOf(name) > 0; +}); +if(bundle) { + return require.toUrl(bundle); +} if(config.paths[name]) { ...
If we find a module in a bundle, then recursively we will begin to calculate the url to this bundle. That way we can even find
packs of packs of modules, if needed.
shim - laying to work with code that does not support AMD. Each shim field contains an object with a description like
load the specified module. For example:
require.config({ shim: { 'jquery.scroll': { deps: ['jquery'], // exports: 'jQuery.fn.scroll', // API init: function() {}, // , exportsFn: function() {} // , init exports } } });
When we load the module for which shim is defined, we must prepare in advance its dependencies and not wait for it
call
define
. Instead of the usual mechanism, the
loadByShim()
function comes into
loadByShim()
function loadByShim(name, path) { var shim = config.shim[name]; return _require(shim.deps || [], function() { return loadScript(name).then(shim.exportsFn || function() { return (shim.init && shim.init()) || getGlobal(shim.exports) }); }, null, path) }
If the module has dependencies, load them, and then connect the module itself. Loading values from it is or
through the
init
function, but if it doesn’t exist or it doesn’t return anything, we use the value of
exports
. There is no name
variable, and the path to it, separated by dots. The
getGlobal
function
getGlobal
fairly standard approach, it can be found,
for example,
in this answer with stackoverlow .
Now we can configure the bootloader to work in a variety of conditions.
See the code for this step on Github .
Special modules
Some modules for their work want to get a little more additional information about the environment. For this
There are special modules built into the library.
require - the require function can be obtained as a module. It differs from the global one in that it loads all modules.
relative to the current module in which it was connected.
module - information about the current module. The most important thing here is the opportunity to find out your settings. Require.js can save settings for modules in among its options:
require.config({ config: { 'my-module': { beAwesome: true } } })
In case
my-module
requests a module by the name of the
module
, its
config
function will return information for it from
corresponding configuration section. This useful feature allows you to keep the settings of all used modules in
one place, as well as customize modules from other developers, if they have provided for it.
exports is an object for exporting data from a module. You can add properties to it so that they are stored as a module value instead of using return in a function.
These three modules are especially useful when using
CommonJS syntax for modules, but its full support is beyond the scope of the article, so we confine ourselves to supporting these entities as special modules.
So, now we will have a new place to look for modules - the variable
locals
:
+var currentModule = path.slice(-1)[0]; ... +if(locals[dependency]) { + return locals[dependency](currentModule); +} if(predefines[dependency]) {
We will extract the module name from the boot history to create the correct local modules by name. Search result for locals
will not be saved, because they each have their own. The simplest require module is the copy of a global function.
locals.require = function() { return require; }
module
should collect information about the current module and pass it to it.
locals.module = module: function(moduleName) { var module = modules[moduleName]; if(!module) { throw new Error('Module "module" should be required only by modules') } return (module.module = module.module || { id: moduleName, config: function () { return config.config[moduleName] || {}; } }); };
exports
adds a return value to the module definition:
locals.exports = function(moduleName) { return (locals.module(moduleName).exports = {}); }
If we want to replace exports entirely (for example, return a function instead of an object), then we can do it through
using
module
:
module.exports = result;
In another way, overwriting exports will not work, because the function arguments in js cannot be overwritten completely, only
partially change.
It remains to learn how to pick up the result from exports, if it is there. Add a check that something was written to it:
})).then(function (modules) { - return factory.apply(null, modules); + var result = factory.apply(null, modules); + return modules[currentModule].module && modules[currentModule].module.exports || result; }, function(reason) {
Now the modules in our system will be able to learn a little more about themselves and return values as convenient for them.
See the code for this step on Github .
Plugin support
To load modules in a non-standard way, you can use plugins. In require to indicate that the module is loading
plugin, add prefix to its name. For example, the
text
plugin allows you to load text data:
require(['text!template.html'], function(template) {
We can use plug-ins to require.js, for this you need to recognize the plug-in prefix and call in this case the correct plugin. Our download logic acquires a new condition:
if(dependency.indexOf('!') > -1) { modules[dependency] = loadWithPlugin(dependency, newPath); }
The
loadWithPlugin()
function will first sort out the dependency to separate the plugin from the module:
var index = dependency.indexOf('!'), plugin = dependency.substr(0, index); dependency = dependency.substr(index+1);
Then you need to download the plugin code itself. We already have a standard loading mechanism, we will use it here too.
return _require([plugin], function(plugin) {
The require function can cache loadable modules, so the plugin will load only once, then it will be taken
from the cache, and our plugin will be immediately ready to go.
The plugin itself is a module that has a load function that must be called for loading. Here is a rough view of the plugin:
define('text', function() { return { load: function(name, require, onLoad, config) {
The
load
function receives certain arguments when calling it:
- name - the name of the module, already without the name of the plugin in the prefix.
- require is a special version of the require function for use inside the plugin. This function has the same methods as
and global, which we considered in the section about utilitarian functions. - onLoad is a function that should be called when the module is loaded and passed as an argument. For the message
about error, the onLoad.error
method is onLoad.error
. For special cases, there is an onLoad.fromText
method that will execute the string passed to it as javascript. This method is used in processor plugins to load CoffeeScript, for example. - config is an object with our settings. A plugin may want to read them in order to understand how to behave.
Since we have all the architecture built on promise, then it is logical to wrap the load in it. And inside
the promise constructor will collect all the necessary methods for the plug-in to work.
return _require([plugin], function(plugin) { return new Promise(function(resolve, reject) { resolve.error = reject; resolve.fromText = function(name, text) {}; plugin.load(dependency, require, resolve, config); }); });
If successful, the plugin will
resolve()
our promise and fill it with the correct value. For other opportunities
need to extend the function
resolve
additional properties. The plugin will be able to cause a
reject
in case of an error by contacting
to the error property, as provided in the require.js API. The
fromText
method
fromText
provided in case the plugin
wants to return not the value of the module directly, but by executing the string as javascript code. So, for example, comes the plugin for CoffeeScript, which loads the coffee-files converts them to js on the fly and gives the result to the execution.
resolve.fromText = function(name, text) { if(!text) { text = name; } var previousModule = pendingModule; pendingModule = {name: dependency, resolve: resolve, reject: reject, path: path}; (new Function(text))(); pendingModule = previousModule; };
The
fromText
function has two call forms. Previously, the module name and code were transmitted, and now it is proposed to transmit only
code. The first method, though declared obsolete, but is used even in official plugins, so you need to support it.
To execute the code, we use the
Funtion()
constructor, but before that we need to set the pendingModule variable.
We have a module in it that is waiting for its download. During execution, it is likely that a call to define will occur, which
will search for the module and throw an error if the module is not found. Prepare the module before the call and gently return to the place
what was there before him. At this time, another module could be loaded without a plug-in, and it is not worth it to break the download.
Plugin support successfully tested on
require-text and
require-cs .
But there is one comment about working with require-coffee. The plugin knows too much about the internal structure of the original.
require and does strange actions
when loading .
load.fromText(name, text); parentRequire([name], function (value) { load(value); });
First, fromText is called to execute the module code and call
define()
, then
require()
happens to
call resolve again. However, the promise is so arranged that it only stores the result of the first call and ignores further
calls to both of their functions - resolve and reject.
Left
pull-request with the removal of unwanted method, but for now it is not
accepted, it is worth learning to ignore such calls on your side. To do this, add the resolved flag and set it once
its true and suppress function calls after that.
After this hack, the CoffeeScript plugin will be set up.
See the code for this step on Github .
We release
The library looks ready to use. Most of the requirements for the AMD loader are met. It remains to make comfortable
connect it to users. Not all browsers support Promise, which we actively use. So, you need to take
aboard your library and use it if it is not in the browser. We will use this
es6-promise-polyfill , because it is small and contains nothing above the standard, and also does not require additional
builds to work in the browser, unlike, for example,
this implementation ,
which still have to prepare.
For the assembly, we will use Gulp, as the most popular assembly system for today. She has plugins for all of our actions.
It remains to name the library and write a colorful readme. So, meet -
require-mini !
The minimal version of require.js with support for only modern browsers (IE9 +). Due to the lack of hacks it has a smaller
size and more understandable code. Installation
bower install require-mini
The first release is ready!
What's next?
In fact, the development is not over. Require.js has many more features, some of which may be useful.
Each link is an issue in a project on Github.
Parallel loading of modules may also be useful. Require.js such
has no functionality and it can be our advantage. Not all browsers can manage this (everywhere,
except IE), but can bring substantial benefits.
Thanks for attention!