С выходом Node.js 6.0 мы из коробки получили готовый набор компонентов для организации честного DI. В данном случае я имею в виду DI, который пытается найти и загрузить нужный модуль только в момент запроса его по имени и находится в глобальной области видимости для текущего модуля, при этом не вмешиваясь в работу сторонних модулей.
Данная статья носит больше исследовательский характер, а ее целью является показать особенности работы Node.js, показать реальную пользу от нововведений ES 2015 и по новому взглянуть на уже имеющиеся возможности JS. Замечу, что этот подход опробован в продакшене, но все же имеет несколько ловушек и требует вдумчивого применения, в конце статьи я опишу это подробнее. Данный DI может легко использоваться в прикладных программах.
Сразу приведу ссылку на репозиторий с рабочим кодом.
И так, давайте опишем основные требования к нашей системе:
- DI не должен исследовать файловую систему перед началом работы.
- DI не должен подключаться вручную в каждом файле.
- 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 не являются магическими переменными, как в других средах. А код модуля просто-напросто оборачивается в функцию, которая потом выполняется с нужными аргументами.
Мы можем сделать собственный загрузчик действующий по тому же принципу, подменить им дефолтный и затем управлять переменными модуля и добавлять свои при необходимости. Отлично, но для DI нам нужно перехватывать обращение к несуществующим переменным. Для этого мы будем использовать 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; } });
Вот, собственно и все. В итоге мы получили самый настоящий DI на Node.js, который незаметен для других модулей, позволяет избежать огромных require-блоков в заголовке файла, ну и, конечно, ускоряет загрузку.
Неочевидные трудности:
- Данный подход требует написания собственного способа генерации кода для расчета покрытия тестами.
- Требуется наличие отдельной точки входа, которая подключает загрузчик DI.
- v8 не оптимизирует код внутри with, но реальное снижение скорости нужно измерять в конкретном случае.
Уже сейчас использовать DI можно в коде тестов, gulp/grunt файлов и т.п.
ссылка на оригинал статьи https://habrahabr.ru/post/283086/
Добавить комментарий