Это перевод статьи Николаса Закаса, создателя ESLint и популярного автора, о дефолтном экспорте в JavaScript. Повествование ведётся от лица автора оригинальной публикации.
- Несколько уточнений
- Экспорт по умолчанию: основы
- Альтернатива: именованный экспорт
- Личные предпочтения
- Проблемы, с которыми я столкнулся
- Ограниченная поддержка инструментами разработки
- Заключение
В январе 2019 года я написал в «Твиттере» о планах отказаться от экспорта по умолчанию в модулях JavaScript. В ответ получил много неожиданных сообщений. Вот текст поста:
В 2019 году планирую отказаться от экспорта по умолчанию в модулях CommonJS/ES6. При импорте дефолтных экспортов шанс ошибиться составляет 50 на 50. Что я импортирую? Это класс? Это функция?
Написал этот пост, когда понял, что львиная доля проблем с модулями в JavaScript может быть связана с экспортом по умолчанию. Не имеет значения, идёт ли речь о модулях JavaScript (или ECMAScript, как многие любят говорить) или CommonJS. Я всякий раз спотыкаюсь при импорте дефолтных экспортов. В ответ на твит получил много вопросов, многие из которых касались того, как именно я пришёл к решению отказаться от экспорта по умолчанию. Эта статья — попытка ответить на все вопросы.
Несколько уточнений
Как и все остальные мои твиты, твит об отказе от дефолтных экспортов был скорее снепшотом или кратким выражением моего мнения, а не публикацией мнения целиком. Нужно прояснить несколько моментов, которые смутили подписчиков в твиттере.
- Упоминание кейса с классами и функциями — только один из примеров проблем, с которыми я сталкиваюсь при использовании экспорта по умолчанию. Это далеко не единственная проблема, которую я решил с помощью именованного экспорта.
- Проблемы, с которыми я столкнулся, проявляются не только при работе с файлами в моих собственных проектах. Они также возникают при импорте библиотек и служебных модулей, которые не принадлежат мне. Это значит, что соглашения об именовании файлов не решают все проблемы.
- Я не говорю, что всем нужно уходить от экспорта по умолчанию. Я говорю, что в модулях, которые пишу я, планирую отказаться от дефолтного экспорта. У вас может быть другое мнение и другие планы, и это хорошо.
Надеюсь, эти уточнения снимут большую часть вопросов, которые возникли у моих подписчиков.
Экспорт по умолчанию: основы
Насколько я знаю, экспорт по умолчанию впервые стал популярным в CommonJS, где модуль можно экспортировать так:
class LinkedList {}
module.exports = LinkedList;
Здесь экспортируется класс LinkedList, но при этом не определяется имя класса для пользователей модуля. Если имя файла — linked-list.js, можно импортировать этот дефолтный экспорт в другой модуль CommonJS так:
const LinkedList = require("./linked-list");
Функция require() возвращает значение, которое я только что назвал LinkedList, чтобы оно соответствовало содержимому файла linked-list.js. Но я также мог назвать его foo или Mountain или любым другим именем.
Популярность экспорта по умолчанию в CommonJS привела к тому, что модули JavaScript разрабатывались с учётом необходимости поддержки этого паттерна:
В ES6 предпочтительно использовать экспорт по умолчанию. Он позволяет использовать приятный синтаксис при импорте экспортированных по умолчанию модулей.
В JavaScript можно экспортировать по умолчанию так:
export default class LinkedList {}
Импорт выглядит так:
import LinkedList from "./linked-list.js";
Опять же, LinkedList в данном случае — произвольное имя. С таким же успехом можно использовать Dog или symphony.
Альтернатива: именованный экспорт
Кроме экспорта по умолчанию, CommonJS и JavaScript поддерживают именованный экспорт. Именованный экспорт позволяет передавать имена функций, классов, переменных в файл, где они будут использоваться.
В CommonJS именованный экспорт выполняется путём присоединения соответствующего имени к объекту exports. Пример ниже.
exports.LinkedList = class LinkedList {};
Импорт выглядит так:
const LinkedList = require("./linked-list").LinkedList;
Опять же, константу можно было назвать как угодно, но я решил, что имя должно совпадать с экспортированным — LinkedList.
В JavaScript именованный экспорт выглядит так:
export class LinkedList {}
Импорт будет таким:
import { LinkedList } from "./linked-list.js";
В этом примере LinkedList — не случайный идентификатор. Он должен совпадать с именованным экспортом LinkedList. Это единственное важное отличие JavaScript от CommonJS в контексте вопросов, которые рассматриваются в этой статье.
Промежуточный итог: в CommonJS и JavaScript поддерживается как экспорт по умолчанию, так и именованный экспорт.
Личные предпочтения
Прежде чем идти дальше, стоит поделиться личными предпочтениями при работе с кодом. Это общие принципы, которых я придерживаюсь, когда пишу код на любом языке программирования:
- Явное лучше неявного. Мне не нравится код с секретами. Например, название функции должно явно отражать то, что функция делает.
- Имена должны быть одинаковыми во всех файлах. Если что-то имеет имя Apple в одном файле, оно не должно называться Orange в другом. Apple всегда остаётся Apple.
- Бросайте ошибки как можно раньше и как можно чаще. Если есть вероятность где-то ошибиться, лучше проверить как можно раньше и по возможности бросить ошибку, которая предупредит о проблеме. Не хочу ждать, пока код выполнится, чтобы понять, что он работает некорректно, а потом искать проблему.
- Меньше решений — быстрее разработка. Многие из моих предпочтений нужны, чтобы принимать меньше решений. Каждое решение, которое нужно принять, замедляет вас. Поэтому вещи типа общепринятых стандартов помогают писать код быстрее. Я хочу решить всё заранее, а потом просто работать.
- Прогулки по неизвестным тропинкам замедляют разработку. Всякий раз, когда вы вынуждены прерывать написание кода и искать что-то, вы гуляете по неизвестной тропинке. Без таких прогулок не обойтись. Но есть много необязательных прогулок по неизвестным путям, которые замедляют вас. Я стараюсь писать код так, чтобы исключить необходимость таких прогулок.
- Когнитивные перегрузки замедляют разработку. Здесь всё просто: чем больше деталей вам надо помнить, чтобы продуктивно писать код, тем медленнее вы разрабатываете.
Важный момент: для меня важен фокус на скорости разработки. Поскольку здоровье с годами ухудшается, энергии на работу с кодом остаётся всё меньше. Всё, что помогает сократить время на написания кода без ущерба для результата, я приветствую.
Проблемы, с которыми я столкнулся
С учётом сказанного выше, я сформулировал основные проблемы, с которыми сталкиваюсь при экспорте по умолчанию, и которые можно решить с помощь именованного экспорта.
Что это я импортировал?
Как сказано в моём твите, при использовании экспорта по умолчанию сложно разобраться, что именно я импортирую. Если вы работаете с модулем или файлом, который не знаете, сложно понять, что он возвращает. Пример:
const list = require("./list");
В контексте этого примера попробуйте понять, что такое list. Вряд ли это примитивное значение, но это может быть функция, класс или другой тип объекта. Как я могу узнать это наверняка? Понадобится дополнительное исследование или прогулка по неизвестной тропинке. В этом случае могут понадобиться такие действия:
- Если list.js принадлежит мне, придётся открыть файл и разобраться, что из него экспортируется.
- Если list.js не принадлежит мне, придётся читать документацию.
В любом случае, придётся запомнить дополнительную информацию, чтобы не возвращаться к изучению файла или чтению документации, когда понадобится новый импорт из list.js. При импорте экспортированных по умолчанию сущностей увеличивается когнитивная перегрузка, так как вам приходится либо запоминать дополнительную информацию, либо дополнительно изучать что-то. Это неоптимальная стратегия.
Кто-то скажет, что эту проблему решают IDE, так как они достаточно «умные» для того, чтобы подсказывать, что именно вы импортируете. Я поддерживаю использование IDE, так как они действительно помогают разработчикам. Но требовать от IDE эффективного использования функций языка — проблематичный подход.
Проблема соответствия имён
Использование именованного экспорта требует, чтобы в файлах-пользователях как минимум определялось имя сущности, которая импортируется. Преимущество в том, что я могу легко найти все случаи использования LinkedList в коде и понять, что это один и тот же LinkedList. В отличие от экспорта по умолчанию, где не надо держать в уме имена, при именованном экспорте разработчик прилагает дополнительные когнитивные усилия. Вы должны выработать правильный подход к именованию, а также должны убедиться, что все разработчики, которые работают над приложением, используют одни и те же имена. Конечно, можно разрешить разработчикам использовать разные имена, но это ещё сильнее усилит когнитивную перегрузку.
Импорт именованного экспорта означает как минимум указание канонического имени сущности везде, где она используется. Даже если вы решите переименовать импорт, это решение будет явным, и его невозможно выполнить без указания канонического имени. Пример в CommonJS:
const MyList = require("./list").LinkedList;
Пример в JavaScript:
import { LinkedList as MyList } from "./list.js";
В каждом из этих примеров вы явно указываете, что LinkedList будет использоваться как MyList.
Когда в коде принято согласованное именование, вы можете:
- Искать в кодовой базе информацию об использовании сущности.
- Изменять имя сущности во всей кодовой базе.
Возможно ли это при использовании экспорта по умолчанию и именовании по принципу ad hoc? Догадываюсь, что возможно. Но также думаю, что это будет намного сложнее, к тому же, увеличится вероятность ошибок.
Ad hoc — латинское выражение, которое применяется для обозначения исключительных случаев, специальных процедур, которые не укладываются в общую практику.
Импорт неправильной сущности
Именованный экспорт в модулях JavaScript имеет важное преимущество перед экспортом по умолчанию. Если вы попробуете импортировать несуществующую сущность, при использовании именованного экспорта будет выброшена ошибка. Посмотрите на этот код:
import { LinkedList } from "./list.js";
Если в list.js нету LinkedList, будет выброшена ошибка. Более того, IDE и ESLint определяют недостающие ссылки ещё до выполнения кода.
Ограниченная поддержка инструментами разработки
Если вернуться к IDE, можно напомнить, что WebStorm может помогать разработчику определять импорты. Если вы введёте идентификатор, который не определён в файле, WebStorm автоматически начнёт искать этот идентификатор в именованных экспортах в модулях вашего проекта. В этот момент он может выполнить любое из указанных действий:
- Подчеркнуть идентификатор, который не определён, и показать импорт.
- Автоматически добавить импорт если вы разрешили автоимпорт.
Как видите, WebStorm действительно помогает вам с именованным импортом. Для Visual Studio Code есть плагин с аналогичной функциональностью. А при использовании экспорта по умолчанию эта функциональность не работает, так как канонического имени для сущности, которую вы импортируете, не существует.
Заключение
Я сталкивался с несколькими проблемами, которые снижали продуктивность, когда пользовался экспортом по умолчанию. Хотя среди этих проблем нет непреодолимых, использование именованного экспорта и импорта всё-таки больше подходит мне. Моя продуктивность растёт, когда я делаю вещи явно и использую автоматические инструменты. Именованный экспорт помогает мне, поэтому я буду использовать его в обозримой перспективе. Конечно, я не могу влиять на то, какой экспорт используют разработчики внешних модулей, которые я использую. Но я точно контролирую свои модули, и в них я буду использовать именованный экспорт.
Ещё раз напоминаю, что всё вышесказанное — моё личное мнение, и вы можете считать его недостаточно убедительным. Эта статья написана не для того, чтобы убедить кого-то отказаться от экспорта по умолчанию. Это всего лишь объяснение моего решения не использовать экспорт по умолчанию в модулях, которые я пишу.
Адаптированный перевод статьи Why I've stopped exporting defaults from my JavaScript modules by Nicholas C. Zakas. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.