Повноцінний DI на node.js

З виходом Node.js 6.0 ми з коробки отримали готовий набір компонентів для організації чесного DI. В даному випадку я маю на увазі DI, який намагається знайти і завантажити потрібний модуль тільки в момент запиту його по імені і знаходиться в глобальній області видимості для поточного модуля, при цьому не втручаючись у роботу сторонніх модулів. Написано за мотивами статей Node.JS Позбудься require() назавжди і Завантажувач модулів для node js з підтримкою локальних модулів і завантаження модулів на вимогу.
Дана стаття носить більше дослідний характер, а її метою є показати особливості роботи 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-блоків в заголовку файлу, ну і, звичайно, прискорює завантаження.
Неочевидні труднощі:
  1. Даний підхід вимагає написання власного способу генерації коду для розрахунку покриття тестами.
  2. Вимагається наявність окремої точки входу, яка підключає завантажувач DI.
  3. v8 не оптимізує код всередині with, але реальне зниження швидкості треба вимірювати в конкретному випадку.
Вже зараз використовувати DI можна в коді тестів, gulp/grunt файлів і т. п.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.