Внедрение зависимостей с помощью Vue.js

Как внедрение зависимостей может упростить вам жизнь при работе с приложением Vue.js?

Внедрение зависимостей часто ассоциируется с крупными корпоративными приложениями и сложными фреймворками. Однако на самом деле это намного проще, чем кажется. Фактически, если вы используете Vue.js, вы, вероятно, уже используете инъекцию зависимостей.

Начнем с примера. Вот простой файл main.js:

import Vue from 'vue'
import i18n from './i18n'
import store from './store'
import App from './components/App.vue'
new Vue( {
  el: '#app',
  i18n,
  store,
  render: h => h( App )
} );

Этот код создает приложение Vue.js, которое использует хранилище Vuex для управления состоянием и плагин vue-i18n для поддержки нескольких языков.

Файл i18n / index.js экспортирует объект, содержащий все сообщения на разных языках:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use( VueI18n );
export default new VueI18n( {
  locale: 'en',
  messages: {
    ...
  }
} );

Аналогичным образом store / index.js экспортирует типичный магазин Vuex:

import Vue from 'vue'
import Vuex from 'vuex'
import module1 from './modules/module1'
Vue.use( Vuex );
const state = {
  ...
};
const actions = {
  ...
};
export default new Vuex.Store( {
  state,
  actions,
  modules: {
    module1
  }
} );

Если вы посмотрите на фрагмент кода, который создает корневой компонент Vue, вы увидите, что объекты i18n и store передаются его конструктору. Вуаля, мы только что использовали внедрение зависимостей!

К этим двум объектам может получить доступ любой компонент Vue, просто используя this.$i18n и this.$store. Таким образом, эти компоненты не должны импортировать i18n / index.js и store / index.js. Вот почему мы говорим, что эти зависимости «внедряются» в компоненты Vue.

Внедрение пользовательских зависимостей в компоненты Vue

Предположим, что в приложении также есть модуль под названием services / ajax.js, который является простой оболочкой для выборки:

export default function ajax( url, data = {} ) {
  return fetch( url, {
    method: 'POST',
    body: JSON.stringify( data ),
    headers: { 'Content-Type': 'application/json' }
  } ).then( response => response.json() );
}

Мы можем использовать механизм внедрения зависимостей, чтобы сделать эту функцию доступной в каждом компоненте Vue как this.$ajax.

Сначала нам нужно импортировать функцию и передать ее конструктору корневого компонента Vue. Давайте изменим main.js:

import Vue from 'vue'
import i18n from './i18n'
import ajax from './services/ajax'
import store from './store'
import App from './components/App.vue'
new Vue( {
  el: '#app',
  i18n,
  ajax,
  store,
  render: h => h( App )
} );

Само по себе это не сработает. Нам также нужно создать очень простой миксин Vue, который будет передавать внедренную зависимость всем дочерним компонентам.

Давайте добавим следующий код в начало services / ajax.js:

import Vue from 'vue'
Vue.mixin( {
  beforeCreate() {
    const options = this.$options;
    if ( options.ajax )
      this.$ajax = options.ajax;
    else if ( options.parent && options.parent.$ajax )
      this.$ajax = options.parent.$ajax;
  }
} );

По сути, этот код является тем, что делают и Vuex, и vue-i18n, когда вы устанавливаете их, вызывая Vue.use(). Разберем, как это работает.

Функция миксина beforeCreate() вызывается всякий раз, когда создается новый экземпляр компонента Vue. Объект this.$options содержит все настраиваемые свойства, переданные конструктору компонента Vue. В случае корневого компонента он включает свойство ajax, поэтому мы просто назначаем его this.$ajax. В противном случае мы проверяем, доступен ли он в родительском элементе нового компонента. Таким образом, все компоненты «наследуют» это свойство от своих родителей, вплоть до корневого компонента.

Вы можете спросить, почему мы не можем вместо этого сделать что-то вроде этого:

Vue.prototype.$ajax = ajax;

Это будет иметь аналогичный эффект: вы можете использовать this.$ajax во всех компонентах Vue для доступа к функции ajax().

Однако представленный выше механизм внедрения зависимостей более гибкий. Может быть несколько корневых компонентов Vue, использующих разные реализации функции ajax(), точно так же, как у них могут быть отдельные хранилища Vuex и отдельные наборы сообщений. Это также полезно при написании модульных тестов, потому что вы можете предоставить разные фиктивные зависимости для каждого тестового примера.

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

Заводские функции

Если вы посмотрите на файл i18n / index.js, который мы создали выше, вы увидите, что мы жестко запрограммировали локаль как «en». В реальном приложении эта информация должна откуда-то поступать.

Изменим этот модуль следующим образом:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use( VueI18n );
export default function makeI18n( locale ) {
  return new VueI18n( {
    locale,
    messages: {
      ...
    }
  } );
}

Обратите внимание, что он больше не экспортирует объект VueI18n напрямую. Вместо этого он экспортирует фабричную функцию с именем makeI18n(), которая создает этот объект и позволяет передавать языковой стандарт в качестве аргумента.

Мы можем использовать тот же подход, чтобы настроить URL-адрес сервера, используемого функцией ajax():

export default function makeAjax( baseURL ) {
  return function ajax( url, data = {} ) {
    return fetch( baseURL + url, {
      method: 'POST',
      body: JSON.stringify( data ),
      headers: { 'Content-Type': 'application/json' }
    } ).then( response => response.json() );
  }
}

Теперь модуль экспортирует фабричную функцию с именем makeAjax(), которая при вызове возвращает функцию ajax(), настроенную для использования правильного сервера. Сначала это может показаться немного странным, но это невероятно мощная и полезная функция JavaScript и других функциональных языков.

Чтобы это работало, нам нужно изменить файл main.js следующим образом:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import store from './store'
import App from './components/App.vue'
const i18n = makeI18n( 'en' );
const ajax = makeAjax( 'http://example.com' );
new Vue( {
  el: '#app',
  i18n,
  ajax,
  store,
  render: h => h( App )
} );

Мы просто импортируем фабричные функции, вызываем их с соответствующими параметрами и передаем результаты корневому компоненту Vue.

Внедрение зависимостей в магазин Vuex

Предположим, мы хотим использовать запросы AJAX и интернационализированные сообщения в хранилище Vuex. Мы не можем просто импортировать i18n / index.js и services / ajax.js, потому что теперь они экспортируют только фабричные функции. Нам нужно каким-то образом внедрить объект i18n и функцию ajax(), созданную в main.js, в хранилище Vuex.

Для этого изменим store / index.js так, чтобы он также экспортировал фабричную функцию:

import Vue from 'vue'
import Vuex from 'vuex'
import makeModule1 from './modules/module1'
Vue.use( Vuex );
const state = {
  ...
};
export default function makeStore( i18n, ajax ) {
  return new Vuex.Store( {
    state,
    actions: makeActions( i18n, ajax ),
    modules: {
      module1: makeModule1( i18n, ajax )
    }
  } );
}
function makeActions( i18n, ajax ) {
  return {
    ...
  };
}

Зависимости передаются функции makeStore(). Эта функция, в свою очередь, вызывает другие фабричные функции для создания действий и дочерних модулей магазина. Затем он возвращает созданный объект хранилища Vuex.

Теперь мы должны изменить main.js, чтобы создать магазин и передать ему необходимые зависимости:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import makeStore from './store'
import App from './components/App.vue'
const i18n = makeI18n( 'en' );
const ajax = makeAjax( 'http://example.com' );
const store = makeStore( i18n, ajax );
new Vue( {
  el: '#app',
  i18n,
  ajax,
  store,
  render: h => h( App )
} );

Как видите, внедрение зависимостей очень просто в использовании и не требует каких-либо сложных инструментов или настроек.

Этот метод настолько универсален, что вы можете применять его ко всем видам модулей, от простых служебных функций, таких как ajax(), до очень сложных объектов, таких как хранилище Vuex.

Используя этот подход, простые функции низкого уровня могут быть составлены вместе, чтобы сформировать более сложные функции и модули высокого уровня. Например, мы могли бы реализовать службу аутентификации на основе функции ajax(), а затем создать модуль управления пользователями, который использует как функцию ajax(), так и службу аутентификации.

Я позаимствовал идею использования фабричных функций для реализации внедрения зависимостей из одного из эпизодов Fun Fun Function Внедрение зависимостей без классов Маттиаса Петтера Йоханссона. Это вдохновило меня использовать эту технику в своих приложениях на JavaScript и написать эту статью.

Преимущества внедрения зависимостей

Некоторые из вас могут спросить, действительно ли передача зависимостей фабричным функциям лучше, чем простой импорт необходимых зависимостей?

При использовании этого подхода мне особенно нравится то, что все зависимости четко видны в одном месте. В приведенном выше примере мы знаем, что модуль store зависит от модулей i18n и ajax, просто взглянув на main.js. Нам не нужно изучать реализацию каждого модуля, чтобы выяснить их зависимости.

Также сразу виден порядок, в котором инициализируются модули, и мы можем легко изменить его при необходимости. Когда модули напрямую импортируют другие модули, порядок, в котором они инициализируются, может быть трудно предсказать и еще сложнее изменить.

Это особенно важно, когда инициализация модулей имеет побочные эффекты или когда она должна происходить асинхронно. Например, что, если модулю i18n необходимо получить словарь сообщений с сервера? Когда модули инициализируются с использованием заводских функций, мы можем просто отложить создание остальной части приложения до загрузки модуля i18n.

Код, который использует внедрение зависимостей, также легче использовать повторно. Например, модуль аутентификации, о котором я упоминал ранее, может быть повторно использован в другом приложении, даже если он имеет другую реализацию функции ajax(). Повторное использование модулей с жестко запрограммированными зависимостями намного сложнее.

Наконец, как я уже упоминал, внедрение зависимостей полезно при использовании модульных тестов, поскольку оно упрощает внедрение фиктивных зависимостей в модули, которые мы тестируем.

В целом, я считаю, что внедрение зависимостей - очень полезный метод для разработки приложений JavaScript любого размера. Это определенно не так сложно, как может показаться поначалу, и для этого не требуются какие-либо специальные инструменты или фреймворки.

Вы можете заметить, что в последнем примере языковой стандарт и URL-адрес сервера по-прежнему жестко заданы в файле main.js. В следующей статье я покажу вам, как эффективно передавать такую ​​информацию с сервера в приложение JavaScript.