От автора: внедрение зависимостей (DI) — это шаблон, в котором компоненты, необходимые для запуска кода, могут быть заменены в горячем режиме. Это означает, что зависимости не жестко запрограммированы в реализации и могут изменяться по мере изменения среды.
Благодаря наследованию, DI — это широко используемый шаблон в объектно-ориентированном программировании (ООП), предназначенный для многократного использования кода в различных объектах и классах. Однако основной причиной использования внедрения зависимостей в React является простое моделирование и тестирование компонентов. В отличие от Angular, DI не является обязательным требованием при работе с React, это скорее удобный инструмент, который можно использовать, когда вы хотите что-то улучшить.
Внедрение зависимости в JavaScript
Чтобы проиллюстрировать принципы DI, представьте модуль npm, который предоставляет следующую функцию:
export const ping = (url) => { return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) }) }
Использование функции ping в современном браузере будет работать нормально.
import { ping } from "./ping" ping("https://logrocket.com").then((status) => { console.log(status ? "site is up" : "site is down") })
Но запуск этого кода внутри Node.js вызовет ошибку, потому что fetch не реализована в Node.js. Однако существует множество реализаций fetch и полифиллов для Node.js, которые мы можем использовать. DI позволяет нам превратить fetch в инъекционную зависимость для ping, например:
export const ping = (url, fetch = window.fetch) => { return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) }) }
От нас не требуется указывать для fetch значение по умолчанию window.fetch. Каждый раз, когда мы используем ping нам не требуется интегрировать fetch, что улучшает опыт разработки. Теперь в Node среде мы можем использовать fetch в сочетании с нашей функцией ping, например:
import fetch from "node-fetch" import { ping } from "./ping" ping("https://logrocket.com", fetch).then((status) => { console.log(status ? "site is up" : "site is down") })
Работа с несколькими зависимостями
Если у нас есть несколько зависимостей, невозможно добавлять их как параметры: func (param, dep1, dep2, dep3,…). Вместо этого лучше иметь объект для зависимостей:
const ping = (url, deps) => { const { fetch, log } = { fetch: window.fetch, log: console.log, ...deps } log("ping") return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) }) } ping("https://logrocket.com", { log(str) { console.log("logging: " + str) } })
Наш параметр deps будет распространен на объект реализации и переопределит функции, которые он предоставляет. При деструктуризации этого измененного объекта уцелевшие свойства будут использоваться в качестве зависимостей. Используя этот шаблон, мы можем переопределить одну зависимость, но не другие.
Внедрение зависимостей в React
Работая с React, мы активно используем пользовательские хуки для извлечения данных, отслеживания поведения пользователей и выполнения сложных вычислений. Излишне говорить, что мы не хотим (и не можем) запускать эти хуки во всех средах.
Отслеживание посещения страницы во время тестирования приведет к повреждению наших аналитических данных, а получение данных из реальной серверной части приведет к медленному выполнению тестов.
Тестирование — не единственная такая среда. Такие платформы, как Storybook, упрощают документацию и могут обойтись без использования многих хуков и бизнес-логики.
Внедрение зависимости через props
Возьмем, к примеру, следующий компонент:
import { useTrack } from '~/hooks' function Save() { const { track } = useTrack() const handleClick = () => { console.log("saving...") track("saved") } return <button onClick={handleClick}>Save</button> }
Как упоминалось ранее, следует избегать использования useTrack (и, в более широком смысле, track). Поэтому преобразуем useTrack в зависимость компонента Save через props:
import { useTracker as _useTrack } from '~/hooks' function Save({ useTrack = _useTrack }) { const { track } = useTrack() /* ... */ }
Применяя псевдоним к нашему useTracker, чтобы избежать коллизии имен и используя его в качестве значения по умолчанию для props, мы сохраняем хук в нашем приложении и имеем возможность переопределить его всякий раз, когда возникает необходимость.
Имя _useTracker — одно из многих соглашений об именах: useTrackImpl, useTrackImplementation и useTrackDI — все это широко используемые соглашения при попытке избежать коллизии. Внутри Storybook мы можем переопределить хук как таковой, используя имитацию (mock) реализации.
import Save from "./Save" export default { component: Save, title: "Save" } const Template = (args) => <Save {...args} /> export const Default = Template.bind({}) Default.args = { useTrack() { return { track() {} } } }
Использование TypeScript
При работе с TypeScript полезно сообщить другим разработчикам, что внедрения зависимостей props — это именно то, что нужно, и использовать реализацию typeof для сохранения безопасности типов:
function App({ useTrack = _useTrack }: Props) { /* ... */ } interface Props { /** * For testing and storybook only. */ useTrack?: typeof _useTrack }
Внедрение зависимости через Context API
Благодаря работе с Context API, внедрение зависимостей позволяет почувствовать себя первоклассным пользователем React. Возможность переопределения контекста, в котором наши хуки запускаются на любом уровне компонента, пригодится при переключении сред.
Многие известные библиотеки предоставляют имитированные реализации своих провайдеров с целью тестирования. React Router v5 имеет MemoryRouter, а Apollo Client предоставляет MockedProvider. Но если мы используем подход, основанный на DI, такие подставные провайдеры не нужны.
React Query — яркий тому пример. Мы можем использовать одного и того же поставщика как для разработки, так и для тестирования и передавать его разным клиентам в каждой среде. В процессе разработки мы можем использовать простой queryClient со всеми параметрами по умолчанию без изменений.
import { QueryClient, QueryClientProvider } from "react-query" import { useUserQuery } from "~/api" const queryClient = new QueryClient() function App() { return ( <QueryClientProvider client={queryClient}> <User /> </QueryClientProvider> ) } function User() { const { data } = useUserQuery() return <p>{JSON.stringify(data)}</p> }
При тестировании кода такие функции, как retries, re-fetch при фокусировке окна и время кеширования, можно настроить соответствующим образом.
// storybook/preview.js import { QueryClient, QueryClientProvider } from "react-query" const queryClient = new QueryClient({ queries: { retry: false, cacheTime: Number.POSITIVE_INFINITY } }) /** @type import('@storybook/addons').DecoratorFunction[] */ export const decorators = [ (Story) => { return ( <QueryClientProvider client={queryClient}> <Story /> </QueryClientProvider> ) }, ]
Внедрение зависимостей в React распространяется не только на хуки, но также и на JSX, JSON и все, что мы хотим абстрагировать или изменить при различных обстоятельствах.
Альтернативы внедрению зависимостей
В зависимости от контекста, внедрение зависимостей может быть неподходящим инструментом для работы. Например, в data-fetching хуках, mock лучше использовать с помощью перехватчика (например, MSW) вместо того, чтобы внедрять хуки по всему тестовому коду, а mocking функции остаются громоздким инструментом для решения более серьезных проблем.
Почему вы должны использовать внедрение зависимостей?
Причины использования DI:
Никаких накладных расходов при разработке, тестировании или производстве
Чрезвычайно легко реализовать
Не требует mocking/stubbing библиотеки, потому что они встроены в JavaScript.
Работает для всех ваших потребностей, таких как компоненты, классы и обычные функции.
Причины не использовать DI:
Загромождает ваш импорт и props / API компонентов
Может сбивать с толку других разработчиков
Заключение
В этой статье мы рассмотрели руководство по внедрению зависимостей в JavaScript и обосновали его использование в React для тестирования и документации. Мы использовали Storybook, чтобы проиллюстрировать использование DI, и, наконец, обсудили причины, по которым вы должны и не должны использовать DI в своем коде.
Автор: Simohamed Marhraoui
Источник: blog.logrocket.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен