Rewriting Require.js using Promise. Part 2

  • Tutorial
In the last part, we wrote a small library, which will burn on require.js and which allows you to load AMD modules. It is time to expand its capabilities and make it a complete replacement for the original require.js. Therefore, today we are implementing the possibility of customization, a similar function require.config()and support for plugins, so that all additions to the usual require.js work here too.



Some utilitarian functions



Require not only allows you to load and declare modules, it has a few more utility functions for our convenience.

toUrl is a fairly lightweight function, turns the module name into a file path. While we do not have support for many of the require features,
it 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 truewhen calling from the function loadScript:

el.src = toUrl(name, true);


Thus, we will provide users with the opportunity to build url according to our rules, but we will reserve the monopoly for
downloading 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 the 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. Put our error handling into 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 .

Download Settings



Not always standard behavior will suit everyone. Require.js can be configured through a function require.config(). It is
time for us to support her. We will have a config variable in which we can write our options and use them
in our work. Function will accept the new options and add them to the existing ones, for example, using the library deepmerge ,
found the vast npm.

var config = {};
require.config = function(options) {
  config = deepMerge(config, options);
}

When we have a mechanism for saving settings, we need to figure out which options we need to support.

baseUrl - the base path to the modules, the path to all modules begins with it. By default it is equal to the address of the current
page, that is, its value ./. Add its support to the function require.toUrl().

toUrl = function(name, appendJS) {
-    return './' + name + (appendJS ? '.js' : '');
+    return config.baseUrl + name + (appendJS ? '.js' : '');
}

This option is also useful to us for tests, the modules are in the fixtures folder, and now we can specify it once, and not
repeat it all the time.

urlArgs is a string added to the end of the url of loadable modules. Useful to save a specific version of the modules
in the browser cache.

paths - paths for those files that are separate from the rest of the modules. Usually these are popular libraries,
for example jQuery, which are on the CDN, so it would be nice to declare that the module jqueryshould be loaded by the url of the form
//code.jquery.com/jquery-2.1.3.min.js. We support the presence of a module in 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 path to the module has become more difficult.

  • Take it from paths, if any.
  • 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 is 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 must look for the module
in one of the packs, and if we find it, then load 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 pack, then recursively begin to compute the url before this pack. Thus, we can even find
packs of bundles of modules, if needed.

shim - gasket for working with code that does not support AMD. Each shim field contains an object with a description of how to
load the specified module. For instance:

require.config({
  shim: {
    'jquery.scroll': {
        deps: ['jquery'], // зависимости библиотеки
        exports: 'jQuery.fn.scroll', // глобальный объект с API библиотеки
        init: function() {}, // код инициализации библиотеки, если нужен
        exportsFn: function() {} // функция экспорта, используется вместо пары init и exports
    }
  }
});

When we load a module for which shim is defined, we must prepare its dependencies in advance and not wait for a
call from it define. Instead of the usual mechanism, a function will come into play here.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 a value from it is either
through a function init, and if it does not exist or if it has not returned anything, then we will use the value exports. There is not a
variable name , but a path to it, separated by dots. The function is a getGlobalfairly 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
, special modules are 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 the modules
relative to the current module into which it is connected.

module - information about the current module. The most important thing here is the opportunity to find out your settings. Require.js is able to save settings for modules in among its options:

require.config({
  config: {
    'my-module': {
      beAwesome: true
    }
  }
})


If a my-modulemodule requests by name module, its function configwill return information for it from the
corresponding section of the configuration. This useful feature allows you to keep the settings of all the modules used in
one place, as well as configure the modules of other developers, if they have provided for this.

exports - an object to export data from the module. You can add properties to it so that they are saved as the value of the module instead of using return in the function.

These three modules are especially useful when using the CommonJS syntax for modules, but its full support is beyond the scope of the article, so we restrict ourselves to supporting these entities as special modules.

So, now we will have a new place to search for modules - a 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. The search result for locals
will not be saved, because they each have their own. The simplest module is the require module - this will be a copy of the global function.

locals.require = function() {
  return require;
}


module should collect information about the current module and pass it to him.

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 the entire exports (for example, return a function instead of an object), then we can do this through
using module:

module.exports = result;


It won’t work in another way to overwrite exports, because function arguments in js cannot be completely overwritten, only
partially changed.

It remains to learn how to take the result from exports, if any, there. Add a check that they wrote something in 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 they like.

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 loaded by the
plugin, a prefix is ​​added to its name. For example, the plugin textallows you to download text data:

require(['text!template.html'], function(template) {
  //do with template anything what you want
});


We can use plugins for require.js, for this we need to recognize the plugin prefix and call the correct plugin in this case. Our loading logic takes on a new condition:

if(dependency.indexOf('!') > -1) {
    modules[dependency] = loadWithPlugin(dependency, newPath);
}


loadWithPlugin()First, the function will parse 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

return _require([plugin], function(plugin) {
  //...
});


The require function is able to 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 work.

The plugin itself is a module that has a load function that must be called to load. Here is a sample plugin view:

define('text', function() {
  return {
    load: function(name, require, onLoad, config) {
      //plugin logic
    }
  }
})


The function loadreceives certain arguments when called:

  • 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 the
    global one, which we considered in the section on utility functions.
  • onLoad is a function that should be called at the end of module loading and pass it as an argument. The
    method is called for the error message onLoad.error. For special cases, there is a method onLoad.fromTextthat will execute the string passed to it as javascript. This method is used in processor plugins to load CoffeeScript, for example.
  • config - an object with our settings. The plugin may want to read them in order to understand how to behave.


Since we have the entire architecture built on promise, it is logical to wrap the load in it here too. And inside the
promise-constructor we will collect all the necessary methods for the plugin 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 call resolve()our promise and fill it with the correct value. For other features,
you need to expand the function with resolveadditional properties. The plugin can call rejectin case of an error by accessing
the error property, as provided in the require.js API. The method is fromTextprovided in case the plugin
wants to return not the value of the module directly, but by executing the line as javascript code. So, for example, a CoffeeScript plugin arrives, which loads coffee files, converts them to js on the fly and gives the result for 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;
};


A function fromTexthas two forms of invocation. Previously, the module name and code were passed, but now it is proposed to transfer only the
code. Although the first method is deprecated, it is used even in official plugins, so you need to support it.
We will use the constructor to execute the code Funсtion(), but before that we need to configure the pendingModule variable.
In it, we have a module waiting for its loading. During execution, most likely, a call to define will occur, which
will begin to search for the module and throw an error if it does not find the module. We will prepare the module before the call and carefully return to the place
what was there before it. At this time, another module could be loaded without a plug-in, and it should not break the download.

Plugin support has been tested successfully on require-text and require-cs .

But there is one point about working with require-coffee. The plugin knows too much about the internal structure of the original
require and does strange things when loading .

load.fromText(name, text);               
parentRequire([name], function (value) {
  load(value);
});


First, fromText is called to execute the code of the module and call in it define(), then it happens require()to
call resolve again. However, the promise is so arranged that it saves the result of only the first call and ignores further
calls to both of its functions - resolve and reject.

The pull-request was left with the removal of the unwanted method, but until it is
accepted, it is worth learning to ignore such calls on your side. To do this, add the resolved flag and once set
it to true and suppress function calls after that.

After this hack, the CoffeeScript plugin will work.

See the code for this step on Github .

Make friends



The library looks ready to use. Most of the requirements for the AMD loader are met. It remains to make it convenient to
connect it to users. Not all browsers support Promise, which we actively use. So, you need to take
it on board your library and use it if it is not in the browser. We will use this
es6-promise-polyfill , because it is small and does not contain anything beyond the standard, and also does not require additional
assembly to work in the browser, unlike, for example, this implementation ,
which has yet to be prepared.

We will use Gulp for assembly, as the most popular assembly system today. She has plugins for all of our actions.

  • проверим свой код через gulp-jshint
  • соберем наш код вместе с Promise с помощью gulp-include. Include позволит нам не просто склеить файлы, но еще и обернуть
    их в замыкание, чтобы наши приватные функции не светились снаружи. Получится как-то так:
    (function(global) {
      //= include ../bower_components/es6-promise-polyfill/promise.js
      //= include deepmerge.js
      //= include core.js
      global.define = define;
      global.require = require;
      global.requirejs = require;
    })(window);
    
  • пропустим код через gulp-uglify для сжатия. Кстати, полный упакованный размер библиотеки 5.8 Кб, без Promise остается
    3 Kb. Require.js версии 2.16 весит 16.5 Кб.
  • собранный код протестируем с помощью karma. Мы это делали и раньше, надо лишь интегрировать в сборку. Karma не требует
    плагинов для сборки, интегрируется с gulp самостоятельно. Еще добавим PhantomJS, чтобы тестировать работу с polyfill, так
    как Firefox уже поддерживает его у себя.


It remains to name the library and write a colorful readme. So, meet - require-mini !

Minimalistic version of require.js with support only for modern browsers (IE9 +). Due to the absence 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
does not have such functionality and this may be our advantage. Not all browsers will manage to implement this (everywhere,
except IE), but it can bring significant benefits.

Thanks for attention!

Also popular now: