📜 ⬆️ ⬇️

Full lazyload on node.js

With the release of Node.js 6.0, we got out of the box a ready-made set of components for organizing an honest, lazy loader. In this case, I mean lazyload, which tries to find and load the necessary module only at the time of querying it by name and is in the global scope for the current module, without interfering with the work of third-party modules. Written based on Node.JS articles . Get rid of require () forever and the Module Loader for node js with support for local modules and module loading on demand .


This article is more research in nature, and its goal is to show the features of Node.js, show the real benefits of the innovations of ES 2015 and take a fresh look at the existing JS capabilities. I note that this approach was tested in production, but still has several pitfalls and requires thoughtful application, at the end of the article I will describe this in more detail. This DI can be easily used in application programs.


Immediately give a link to the repository with the working code.


And so, let's describe the basic requirements for our system:



It will work like this:


// script.js speachModule.sayHello(); 

 // deps/speach-module.js exports.sayHello = function() { console.log('Hello'); }; 

Pseudo-global scope


What is pseudo-global scope? This is the scope of variables available from any file, but only within the current module. Those. it is not available to modules from node_modules, or lying above the module root. But how to achieve this? To do this, we need to examine the Node.js module loading system.


Create an exception.js file:


 throw 'test error'; 

And then fulfill it:


 node exception.js 

Look at the position label of the error in the trace, there is clearly not what you expected to see.


The fact is that the system of loading modules of Node.js itself when connecting a module, its contents turn into a function :


 NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ]; 

As you see, exports, require, dirname, filename are not magic variables, as in other environments. And the module code simply turns into a function, which is then executed with the necessary arguments.


We can make our own loader acting on the same principle, replace it with a default one and then manage the variables of the module and add our own if necessary. Great, but we need to intercept the call to non-existent variables. To do this, we will use with , which will act as an intermediary between the global and the current scope, and for each module to get the correct scope, we will use the scopeLookup method, which will look for the scope.js file in the module root and return it for all files within the project , and for the others to transfer global .


Quite often, with are criticized for the obviousness and subtlety of errors associated with the substitution of variables. But with proper use, the with behaves more than predictably.

This is what a wrapper can look like now:


 var wrapper = [ '(function (exports, require, module, __filename, __dirname, scopeLookup) { with (scopeLookup(__dirname)) {', '\n}});' ]; 

The complete loader code in the repository with an example.


As I wrote above, the scope itself is stored in the file scope.js . This is to make the process of making and tracking changes in our scope more obvious.


Load modules on demand


Good. Now we have a scope.js file in which the export object contains the values ​​of the pseudo-global scope. Things are easy: let's replace the exports object with the Proxy instance, which we will teach to load the necessary modules on the fly:


 const fs = require('fs'); const path = require('path'); const decamelize = require('decamelize'); //   scope const scope = {}; module.exports = new Proxy(scope, { has(target, prop) { if (prop in target) { return true; } if (typeof prop !== 'string') { return; } var filename = decamelize(prop, '-') + '.js'; var filepath = path.resolve(__dirname, 'deps', filepath); return fs.existsSync(filepath); }, get(target, prop) { if (prop in target) { return target[prop]; } if (typeof prop !== 'string') { return; } var filename = decamelize(prop, '-') + '.js'; var filepath = path.resolve(__dirname, 'deps', filename); if (fs.existsSync(filepath)) { return scope[prop] = require(filepath); } return null; } }); 

That's all. As a result, we got a real lazyload on Node.js, which is invisible to other modules, allows us to avoid huge require-blocks in the file header, and, of course, allows us to speed up system initialization.


Uneasy difficulties:


  1. This approach requires writing your own code generation method for calculating test coverage.
  2. Requires a separate entry point that connects the bootloader.

Already now you can use such a bootloader in test code, gulp / grunt files, etc.


')

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


All Articles