предисловие
существуетПосле инверсии контроля описана выше, я собирался написать статью, чтобы представить распространенный паттерн инверсии управления — внедрение зависимостей. Просматривая информацию, я нашел хорошую статьюDependency injection in JavaScript, не заморачивайтесь, переводите по своему разумению и наслаждайтесь хорошим текстом.
Мне нравится цитировать фразу «программирование — это управление сложностью». Вероятно, вы также слышали, что компьютерный мир — это огромная абстрактная структура.
Мы просто упаковываем вещи и повторяем, чтобы произвести новые инструменты. Подумайте об этом, языки программирования, которые мы используем, включают в себя встроенную функциональность, которая может быть абстрагирована от других низкоуровневых операций, включая javascript, который мы используем.
Рано или поздно нам всем придется использовать абстракции, разработанные другими разработчиками, а это значит, что нам придется полагаться на чужой код.
Я хотел бы использовать модули без зависимостей, которые, очевидно, сложно достичь.
Даже если вы создаете красивые компоненты, похожие на черный ящик, всегда есть место, где все части подходят друг к другу.
Вот здесь-то и вступает в игру внедрение зависимостей.В настоящее время крайне необходима возможность эффективного управления зависимостями.Эта статья суммирует первоначальные взгляды автора на этот вопрос.
Цель
Предположим, у нас есть два модуля, служба, которая делает ajax-запросы, и маршрут:
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
Вот еще одна функция, которая зависит от вышеуказанного модуля:
var doSomething = function(other) {
var s = service();
var r = router();
};
Чтобы сделать вещи немного интереснее, функция должна принимать один параметр. Конечно, мы могли бы использовать приведенный выше код, но он менее гибкий.
Если мы хотим использовать ServiceXML, ServiceJSON или хотим имитировать некоторые тестовые модули, чтобы мы не могли каждый раз редактировать тело функции.
Чтобы решить этот статус-кво, сначала мы выдвинем зависимости в качестве параметра, передаваемого функции, следующим образом:
var doSomething = function(service, router, other) {
var s = service();
var r = router();
};
Таким образом мы передаем конкретный экземпляр требуемого модуля.
Однако возникает новая проблема: подумайте, если функция dosomething вызывается во многих местах, и если есть третья зависимость, мы не можем изменить все места, где вызывается doSomething.
Возьмите небольшой каштан:
Предположим, мы используем doSomething во многих местах:
//a.js
var a = doSomething(service,router,1)
//b.js
var b = doSomething(service,router,2)
// 假如依赖条件更改了,即doSomething需要第三个依赖,才能正常工作
// 这时候就需要在上面不同文件中修改了,如果文件数量够多,就不合适了。
var doSomething = function(service, router, third,thother) {
var s = service();
var r = router();
//***
};
Поэтому нам нужен инструмент, который поможет нам управлять зависимостями.
Это проблема, которую хочет решить инжектор зависимостей. Давайте посмотрим, чего мы хотим достичь:
- Может регистрировать зависимости
- Инжектор должен принимать функцию и возвращать функцию, которая получила требуемый ресурс.
- Мы не должны писать сложный код, нам нужен короткий и элегантный синтаксис
- Инжектор должен сохранять объем входящей функции
- Передаваемая функция должна иметь возможность принимать пользовательские параметры, а не только описанные зависимости.
Это кажется идеальным списком, давайте попробуем его реализовать.
requirejs/AMD способ
Возможно, вы слышали о requirejs — отличном решении для управления зависимостями.
define(['service', 'router'], function(service, router) {
// ...
});
Идея состоит в том, чтобы сначала объявить необходимые зависимости, а затем начать писать функции. Порядок параметров здесь важен.
Давайте попробуем написать модуль с именем инжектор, который принимает тот же синтаксис.
var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
Сделайте здесь небольшую паузу, чтобы объяснить суть doSomething и использовать expect.js в качестве библиотеки утверждений, чтобы убедиться, что мой код работает должным образом.
воплощает немногоTDD (разработка через тестирование)режим разработки.
Ниже показано начало нашего модуля инжектора, одноэлементный шаблон — хороший выбор, поэтому он может хорошо работать в разных частях нашего приложения.
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
}
}
С точки зрения кода это действительно очень простой объект.
Есть две функции и переменная, которая является очередью хранения.
Что нам нужно сделать, так это проверить массив зависимостей deps и найти ответ в очереди зависимостей.
Осталось только вызвать метод .apply для объединения аргументов, переданных функции.
//处理之后将依赖项当做参数传入给func
resolve: function(deps, func, scope) {
var args = [];
//处理依赖,如果依赖队列中不存在对应的依赖模块,显然该依赖不能被调用那么报错,
for(var i=0; i<deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
args.push(this.dependencies[d]);
} else {
throw new Error('Can\'t resolve ' + d);
}
}
//处理参数,将参数拼接在依赖后面,以便和函数中参数位置对应
return function() {
func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
}
}
Если область действия существует, ее можно эффективно передать. Array.prototype.slice.call(arguments, 0) преобразует аргументы (подобные массиву) в настоящий массив.
Пока он выглядит довольно хорошо и может пройти тест.
Текущая проблема в том, что нам приходится прописывать нужные зависимости дважды, причем порядок менять нельзя, а лишние параметры могут быть только в конце.
реализация отражения
Согласно Википедии, отражение — это способность программы проверять и изменять структуру и поведение объектов во время выполнения.
Короче говоря, в контексте js это относится к чтению и анализу исходного кода объекта или функции.
Посмотрите на doSomething в начале, если вы используете doSomething.toString(), вы можете получить следующий результат.
"function (service, router, other) {
var s = service();
var r = router();
}"
Этот способ преобразования функции в строку дает нам возможность получить ожидаемые аргументы. И что еще более важно, их имя.
Вот как реализовано внедрение зависимостей Angular, я взял регулярное выражение из Angular для получения аргументов:
/^function\s*[^\(]*\(\s*([^\)]*)\)/m
Таким образом, мы можем изменить метод разрешения:
tip
Здесь я приведу тестовый пример для лучшего понимания.
var doSomething = injector.resolve(function(service, other, router) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
Продолжаем смотреть на нашу реализацию.
resolve: function() {
// agrs 传给func的参数数组,包括依赖模块及自定义参数
var func, deps, scope, args = [], self = this;
// 获取传入的func,主要是为了下面来拆分字符串
func = arguments[0];
// 正则拆分,获取依赖模块的数组
deps = func.toString().match(/^functions*[^(]*(s*([^)]*))/m)[1].replace(/ /g, '').split(',');
//待绑定作用域,不存在则不指定
scope = arguments[1] || {};
return function() {
// 将arguments转为数组
// 即后面再次调用的时候,doSomething("Other");
// 这里的Other就是a,用来补充缺失的模块。
var a = Array.prototype.slice.call(arguments, 0);
//循环依赖模块数组
for(var i=0; i<deps.length; i++) {
var d = deps[i];
// 依赖队列中模块存在且不为空的话,push进参数数组中。
// 依赖队列中不存在对应模块的话从a中取第一个元素push进去(shift之后,数组在改变)
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
//依赖当做参数传入
func.apply(scope || {}, args);
}
}
При использовании этого регулярного выражения для обработки функций мы получаем следующие результаты:
["function (service, router, other)", "service, router, other"]
Все, что нам нужно, это второй элемент, как только мы очистим массив и разделим строку, мы получим массив зависимостей.
Основные изменения заключаются в следующем:
var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
Таким образом мы зациклим зависимость, если что-то упустили, то можем попробовать получить из объекта Arguments. К счастью, когда массив пуст, метод SHIFT просто возвращает undefined, а не бросает. Таким образом, новая версия использования выглядит следующим образом:
//不用在前面声明依赖模块了
var doSomething = injector.resolve(function(service, other, router) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
Таким образом, нет необходимости повторять декларацию, а порядок можно изменить. Мы копируем магию Angular.
Однако это не идеально, сжатие нарушает нашу логику, что является большой проблемой при внедрении отражения.
У нас нет возможности разрешить эти зависимости, потому что сжатие изменяет имена параметров. Например:
// 显然根据key来匹配就是有问题的了
var doSomething=function(e,t,n){var r=e();var i=t()}
Решение команды Angular выглядит следующим образом:
var doSomething = injector.resolve(['service', 'router', function(service, router) {
}]);
Выглядит так же, как и начало require.js.
Автор лично не может найти лучшего решения, чтобы приспособить оба пути. Окончательное решение выглядит так:
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function() {
var func, deps, scope, args = [], self = this;
// 该种情况是兼容形式,先声明
if(typeof arguments[0] === 'string') {
func = arguments[1];
deps = arguments[0].replace(/ /g, '').split(',');
scope = arguments[2] || {};
} else {
// 反射的第一种方式
func = arguments[0];
deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
scope = arguments[1] || {};
}
return function() {
var a = Array.prototype.slice.call(arguments, 0);
for(var i=0; i<deps.length; i++) {
var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {}, args);
}
}
}
Теперь resolve принимает два или три параметра, если их два, то это первый, который мы прописали, если три, то первый параметр будет разобран и залит в deps.
Вот тестовый пример (я всегда думал, что лучше оставить этот пример впереди):
// 缺失了一项模块other
var doSomething = injector.resolve('router,,service', function(a, b, c) {
expect(a().name).to.be('Router');
expect(b).to.be('Other');
expect(c().name).to.be('Service');
});
// 这里传的Other将会用来拼凑
doSomething("Other");
Вы можете заметить, что в arguments[0] есть один элемент, предназначенный только для проверки функции заполнения.
непосредственный впрыск в прицел
Иногда мы используем третий метод внедрения, который включает в себя работу области действия (или другого имени, объекта this), который используется нечасто.
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
scope = scope || {};
for(var i=0; i<deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
//区别就在这里了,直接将依赖加到scope上
//这样就可以直接在函数作用域中调用了
scope[d] = this.dependencies[d];
} else {
throw new Error('Can\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
}
}
}
Что мы делаем, так это добавляем зависимости в область действия.Преимущество этого в том, что нет необходимости добавлять зависимости к параметрам, и это уже является частью области видимости функции.
var doSomething = injector.resolve(['service', 'router'], function(other) {
expect(this.service().name).to.be('Service');
expect(this.router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
заключительные замечания
Внедрение зависимостей — это одна из тех вещей, которые мы все делаем, не осознавая этого. Даже если вы не слышали его, вы, вероятно, использовали его много раз.
Благодаря этой статье я углубил свое понимание этой знакомой и незнакомой концепции и надеюсь помочь нуждающимся учащимся. В конце концов, личные возможности ограничены, и вы можете указать на любые ошибки в переводе и вместе добиться прогресса.
Еще раз спасибо автору оригиналаисходный адрес.
Для получения дополнительных статей, пожалуйста, перейдите в мой блог