Как стать автором
Обновить

Полноценный lazyload на node.js

Время на прочтение3 мин
Количество просмотров17K

С выходом Node.js 6.0 мы из коробки получили готовый набор компонентов для организации честного ленивого загрузчика. В данном случае я имею в виду lazyload, который пытается найти и загрузить нужный модуль только в момент запроса его по имени и находится в глобальной области видимости для текущего модуля, при этом не вмешиваясь в работу сторонних модулей. Написано по мотивам статей Node.JS Избавься от require() навсегда и Загрузчик модулей для node js с поддержкой локальных модулей и загрузки модулей по требованию.


Данная статья носит больше исследовательский характер, а ее целью является показать особенности работы Node.js, показать реальную пользу от нововведений ES 2015 и по новому взглянуть на уже имеющиеся возможности JS. Замечу, что этот подход опробован в продакшене, но все же имеет несколько ловушек и требует вдумчивого применения, в конце статьи я опишу это подробнее. Данный DI может легко использоваться в прикладных программах.


Сразу приведу ссылку на репозиторий с рабочим кодом.


И так, давайте опишем основные требования к нашей системе:


  • Загрузчик не должен исследовать файловую систему перед началом работы.
  • Загрузчик не должен подключаться вручную в каждом файле.
  • Загрузчик не должен вмешиваться в работу сторонних модулей из директории node_modules.

Работать это будет приблизительно так:


// script.js
speachModule.sayHello();

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

Псевдо-глобальная область видимости


Что такое псевдо-глобальная область видимости? Это область видимости переменных доступных из любого файла, но только внутри текущего модуля. Т.е. она не доступна модулям из node_modules, или лежащим выше корня модуля. Но как этого добиться? Для этого нам понадобится изучить систему загрузки модулей Node.js.


Создайте файл exception.js:


throw 'test error';

А затем исполните его:


node exception.js

Посмотрите на метку позиции ошибки в трейсе, там явно не то что вы ожидали увидеть.


Дело в том, что система загрузки модулей самого Node.js при подключении модуля его содержимое оборачивается в функцию:


NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];

Как видите exports, require, dirname, filename не являются магическими переменными, как в других средах. А код модуля просто-напросто оборачивается в функцию, которая потом выполняется с нужными аргументами.


Мы можем сделать собственный загрузчик действующий по тому же принципу, подменить им дефолтный и затем управлять переменными модуля и добавлять свои при необходимости. Отлично, но нам нужно перехватывать обращение к несуществующим переменным. Для этого мы будем использовать with, который будет выступать посредником между глобальной и текущей областями видимости, а чтобы каждый модуль получил правильный scope, мы будем использовать метод scopeLookup, который будет искать файл scope.js в корне модуля и возвращать его для всех файлов внутри проекта, а для остальных передавать global.


Довольно часто with критикуют за неочевидность и трудноуловимость ошибок, связанных с подменой переменных. Но при надлежащем использовании with ведет себя более чем предсказуемо.

Вот так может выглядеть обертка теперь:


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

Полный код загрузчика в репозитории с примером.


Как я уже писал выше, сам scope хранится в файле scope.js. Это нужно для того, чтобы сделать более очевидным процесс внесения и отслеживания изменений в нашей области видимости.


Подгрузка модулей по требованию


Хорошо. Теперь у нас есть файл scope.js, в котором объект export содержит значения псевдо-глобальной области видимости. Дело за малым: заменим объект exports на экземпляр Proxy, который мы обучим загружать нужные модули на лету:


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;
    }
});

Вот, собственно и все. В итоге мы получили самый настоящий lazyload на Node.js, который незаметен для других модулей, позволяет избежать огромных require-блоков в заголовке файла, ну и, конечно, позволяет ускорить инициализацию системы.


Неочевидные трудности:


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

Уже сейчас использовать такой загрузчик можно в коде тестов, gulp/grunt файлов и т.п.

Теги:
Хабы:
Всего голосов 20: ↑16 и ↓4+12
Комментарии22

Публикации

Истории

Работа

Ближайшие события