Pull to refresh

Node.JS Загрузка модулей по требованию

Reading time 4 min
Views 14K
JavaScript *Node.JS *
Sandbox
Иногда, например, при обработке больших массивов данных, для использования максимума ресурсов окружения и сокращения общего затраченного времени работы, нам приходится использовать конкурирующие процессы, которые одновременно выполняют однотипные задачи над разными объектами.

Предположим, мы разрабатываем простой пакет для npm. Назовём его, например, storage (хранилище). Заранее предусмотрим возможность использования одного из нескольких типов хранилищ, например, FsStorage (файловое хранилище), MysqlStorage (MySQL-хранилище), MongoStorage (Mongo-хранилище).

Накидаем содержимое исходных кодов нашего пакета (под спойлером):

Примерный набросок исходного кода проекта
  • package.json:

    {
        "name": "storage",
        "version": "0.1.0",
        "main": "./lib/index.js",
        "dependencies": {
            "mysql": "*",
            "mongoose": "*"
        }
    }
  • lib/index.js:

    module.exports = {
        FsStorage: require('./fs-storage.jsx'),
        MysqlStorage: require('./mysql-storage.jsx'),
        MongoStorage: require('./mongo-storage.jsx')
    };
    
  • lib/fs-storage.js:

    var fs = require('fs');
    
    module.exports = FsStorage;
    
    function FsStorage() {
        // init code...
    }
    
  • lib/mysql-storage.js:

    var mysql = require('mysql');
    
    module.exports = MysqlStorage;
    
    function MysqlStorage() {
        // init code...
    }
    
  • lib/mongo-storage.js:

    var mongoose = require('mongoose');
    
    module.exports = MongoStorage;
    
    function MongoStorage() {
        // init code...
    }
    


Непосредственный код зависимостей mysql и mongoose для демонстрации нам необязателен. Поэтому, разместим вместо кода mysql и mongoose заглушки (под спойлером):

Исходный код заглушек модулей mysql и mongoose
  • node_modules/mysql/index.js:
    console.log('MySQL module loaded');
    
  • node_modules/mongoose/index.js:
    console.log('Mongoose module loaded');
    

Тогда файловая структура пакета будет выглядеть следующим образом (под спойлером):

Макет дерева файловой структуры
storage/
    ├ lib/
    │    ├ index.js
    │    ├ fs-storage.js
    │    ├ mongo-storage.js
    │    └ mysql-storage.js
    ├ node_modules/
    │    ├ mongoose/
    │    │    └ index.js
    │    └ mysql/
    │         └ index.js
    └ package.json

Теперь представим, что у нас есть мега-большая задача, которую будем выполнять с помощью нескольких дочерних процессов, и по ходу выполнения нам необходимо использовать хранилище в виде файлов.

var storage = require('storage');
var fsStorage = new storage.FsStorage();

Запускаем и наблюдаем: каждый дочерний процесс занимает памяти на порядок больше, чем ожидалось. А если количество конкурирующих процессов перевалит за сотню, и это не единственная задача, которая выполняется на сервере в реальном времени?!

Тут мы и понимаем, что, например, при использовании файлового хранилища, незачем загружать как библиотеку по управлению базами данных MySQL, так и ODM-клиент Mongo.

Сразу после вызова require('storage') в консоль выводятся сообщения:

MySQL module loaded
Mongoose module loaded

Поэкспериментировав с методом Object.defineProperty(), я добился удивительного результата, который оформил в виде функции demandProperty():

function demandProperty(obj, name, modPath) {
    Object.defineProperty(obj, name, {
        configurable: true,
        enumerable: true,
        get: function() {
            var mod = require(modPath);
            Object.defineProperty(obj, name, {
                configurable: false,
                enumerable: true,
                value: mod
            });
            return mod
        }
    })
}

Принцип работы прост: Вместо прямой ссылки, например, на MysqlStorage(), создается акцессор (геттер). При любом запросе к акцессору, срабатывает require(), а сам акцессор возвращает результат require(). Кроме того, с помощью того же Object.defineProperty() мы устанавливаем обратно прямую ссылку на тот же результат require() вместо акцессора (то есть, на MysqlStorage()). Так все запросы (разумеется, кроме первого) будут работать с той же скоростью и надежностью от утечек, как если бы мы оставили классический require().

Изменим lib/index.js. Заменим:

module.exports = {
    FsStorage: require('./fs-storage.jsx'),
    MysqlStorage: require('./mysql-storage.jsx'),
    MongoStorage: require('./mongo-storage.jsx'),
};
на:

demandProperty(module.exports, 'FsStorage', './fs-storage.jsx');
    demandProperty(module.exports, 'MysqlStorage', './mysql-storage.jsx');
    demandProperty(module.exports, 'MongoStorage', './mongo-storage.jsx');

И используем:

var storage = require('storage');

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Getter],
    MysqlStorage: [Getter],
    MongoStorage: [Getter] }
*/

console.log(util.inspect(storage.FsStorage.name));
// =>  'FsStorage'

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Function: FsStorage],
    MysqlStorage: [Getter],
    MongoStorage: [Getter] }
*/

var mysql = new storage.MysqlStorage();
// =>  MySQL module loaded
console.log(util.inspect(mysql));
// =>  '{}'

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Function: FsStorage],
    MysqlStorage: [Function: MysqlStorage],
    MongoStorage: [Getter] }
*/

Есть ещё одна тонкость. Если определение функции demandProperty() вынести за пределы модулей в папке lib, последним аргументом необходимо передавать полный путь до модуля, иначе require() будет искать модуль в той папке, где определен demandProperty():

demandProperty(module.exports, 'FsStorage', path.resolve(__dirname, './fs-storage.jsx'));
demandProperty(module.exports, 'MysqlStorage', path.resolve(__dirname, './mysql-storage.jsx'));
demandProperty(module.exports, 'MongoStorage', path.resolve(__dirname, './mongo-storage.jsx'));


Всем удачных экспериментов!
Tags:
Hubs:
Total votes 12: ↑10 and ↓2 +8
Comments 20
Comments Comments 20

Posts