📜 ⬆️ ⬇️

Rewrite Require.js using Promise. Part 2

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 === 'function') { errback(reason); } +require.onError(reason); -console.error(reason); 


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.



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) { //do with template anything what you want }); 


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) { //plugin logic } } }) 


The load function receives certain arguments when calling it:



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 --save 


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!

Source: https://habr.com/ru/post/252849/


All Articles