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

Spread the love

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

Перевод статьи Michał Męciński Dependency injection with 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. Вот почему мы говорим, что эти зависимости «внедряются» (injected) в компоненты Vue.

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

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

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 store

Давайте предположим, что мы хотим использовать запросы AJAX и интернационализированные сообщения в Vuex store. Мы не можем просто импортировать 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 store.

Теперь мы должны изменить 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, Dependency Injection without classes, от Mattias Petter Johansson. Это вдохновило меня использовать эту технику в моих приложениях JavaScript и написать эту статью.

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

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

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

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

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

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

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

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


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


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.