Подключение модулей без явного использования require или аналог namespace

в 16:35, , рубрики: javascript, node.js, require, загрузка файлов, метки: , ,

При работе с node.js часто приходится подключать файлы, расположенные не в текущей директории, и даже не в поддереве текущего каталога. Поэтому часто в модулях можно наблюдать что-то вроде

var obj = require('../../../a/b/c/someModule');

Для меня гораздо удобнее подключать файлы относительно корня проекта (модуля), например

var obj = require(base_dir + '/a/b/c/someModule');

Однако и в этом случае довольно много писанины и необходимо откуда-то получать base_dir.
Для себя я нашел решение проблемы, позволяющее больше не писать require и не узнавать base_dir:

var obj = lib.a.b.c.someModule;

Кому интересно решение — прошу под кат.

Идея решения

Решение проблемы — создание хитрого «класса» (да простят меня гуру javascript за такое слово), свойствами которого являются директории и файлы указанной при создании класса директории. Если свойство-директория не является модулем, то данное свойство также является объектом нашего класса, свойствами которого являются директории и файлы этой директории.

Чтобы стало понятнее — приведу пример. Предположим у нас имеется следующая структура проекта:
Подключение модулей без явного использования require или аналог namespace
Содержимое файлов:
index.js

var LazyLoader = require('./LazyLoader');
global.lib = new LazyLoader(__dirname);

var foo = new lib.a.b.Foo();
console.log(foo.name);
console.log(foo.bar.name);

Foo.js

module.exports = function () {
    this.name = 'Foo';
    this.bar = new lib.a.c.Bar();
};

Bar.js

module.exports = function () {
    this.name = 'Bar';
};
LazyLoader.js

var fs = require('fs');
var path = require('path');

function getter(key, dir, file) {
    delete this[key];

    var fullPath = path.join(dir, file);
    try {
        require.resolve(fullPath);
    } catch (e) {
        if (fs.existsSync(fullPath)) {
            this[key] = new LazyLoader(path.join(dir, file));
        }
        return this[key];
    }

    this[key] = require(fullPath);
    return this[key];
}

function LazyLoader(dir) {

    var loader = this;

    if (!fs.existsSync(dir)) {
        return;
    }
    fs.readdirSync(dir).forEach(function(file) {
        var key = file.replace(/.js$/, '');
        loader.__defineGetter__(key, getter.bind(loader, key, dir, file))
    });
}

module.exports = LazyLoader;

При запуске node index.js в консоли распечатается (как и ожидалось)

Foo
Bar

В данном примере я использовал глобальную переменную lib. Если вы противник глобальных переменных (или вы просто разрабатываете отдельный публичный модуль) — то можно отказаться от глобальной переменной — в Foo.js придется написать, например, так:

var path = require('path');
var LazyLoader = require('../../LazyLoader');
var lib = new LazyLoader(path.join(__dirname, '../../'));

module.exports = function () {
    this.name = 'Foo';
    this.bar = new lib.a.c.Bar();
};

И если у вас всего 1-2 подключаемых файла, то возможно данный подход покажется вам неуместным, но при большем количестве — вы должны ощутить существенное облегчение.
Кроме того вы можете сохранять ссылки на директории для сокрашения записи, например:

var lib = new LazyLoader(__dirname);
var logLig = lib.dir1.dir2.dir3.log;
var f1 = new logLib.ConsoleLogger();
var f2 = new logLib.FileLogger();
// вместо
// var f1 = new lib.dir1.dir2.dir3.log.ConsoleLogger();
// var f2 = new lib.dir1.dir2.dir3.log.FileLogger();

//  и вместо
// var f1 = new require(__dirname + '/dir1/dir2/dir3/log/ConsoleLogger)();
// var f2 = new require(__dirname + '/dir1/dir2/dir3/log/FileLogger)();

Заключение

1. Указанный подход позволяет имитировать аналог namespace в других языках.

2. Приведенный в примере LazyLoader легко расширить для работы с несколькими директориями, например, для смешвания или переопределения модулей:

var lib = new LazyLoader('globalLibDir', 'applicationSpecificLibDir');

3. Если Вы нашли неточности, опечатки или у вас есть альтернативные решения подключения модулей — пишите в ЛС.

Автор: Sirian

Источник

Поделиться

* - обязательные к заполнению поля