Как сделать экспорт файла js

Добавил пользователь Владимир З.
Обновлено: 10.09.2024

Попытка разобраться, в чем различие между и , основанная на статье Understanding module.exports and exports in Node.js.

Что такое и в Node.js

Как web-разработчики, мы часто сталкиваемся с ситуацией, когда необходимо иметь дело с малознакомым кодом. И тогда сам собою возникает логичный вопрос - сколько времени мне потребуется для того, чтобы разобраться с чужим кодом и понять принцип его работы?

Типичный ответ на этот вопрос - ровно столько, чтобы начать самому писать код; а затем вернуться к изучению этой темы позже, когда позволит на это время. Ну что-же, как мне кажется, пришло время получше разобраться с такими понятиями, как и в Node.js. Спешу поделиться с вами тем, что я узнал по этому вопросу.

Что такое модуль (module)

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

Для понимания вышесказанного лучше всего создать пример приложения под Node.js. Давайте создадим файл с именем , внутри которого размещены две функции:

Экспорт модуля

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

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

  • представьте себе, что эта строка существует в качестве первой линии кода в :
  • видоизменим обе функции в файле с помощью выражения таким образом, чтобы они были доступны для внешних файлов (модулей):

В приведенном выше коде можно было бы заменить выражение на и получить точно такой же результат.

Если этот момент кажется вам непонятным, то помните, что выражение и выражение ссылаются на один и тот же объект.

Импортирование модуля

Давайте сделаем методы модуля общедоступными для другого файла (модуля) с именем . Этот процесс также разобьем пошагово для более лучшего понимания:

  • в Node.js используется команда для импортирования одного модуля в другой модуль:
  • давайте подключим модуль в модуль :

Приведенная выше строка кода равнозначна нижеследующему коду:

  • теперь можно использовать функции модуля внутри модуля как методы объекта :

Отличительные моменты

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

Нижеприведенный пример поможет разобраться в данном вопросе:

Теперь сделаем подключение модуля в модуль :

На данный момент в нашем примере ничего не поменялось. В переменную помещается код, доступный из модуля . Не более того.

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

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

Чтобы отследить ошибки при использовании модуля , можно вывести их в консоль:

Заключение

Импорт и экспорт модулей в Node.js является ежедневной задачей. Я надеюсь, что благодаря этой статье стала ясна разница между командой и командой . Более того, если у вас когда-либо произойдет ошибка при доступе к общедоступным методам модуля в будущем, то я надеюсь, что у вас есть лучшее понимание того, почему может возникнуть эти ошибки.

Заключение автора перевода

Если честно - прочитал статью и даже потрудился перевести ее, а вот разницы между и не заметил. Я хочу сказать, что автор этой статьи (как мне кажется) так и не показал разницы между ними. Могу ошибаться, конечно и буду рад комментариям.

UPD. Вопрос снят, так как решен. В принципе, существование этой и предыдущей статьи уже не надобно, так как есть отличный скринкаст от Ильи Кантора по Node.js, где раскрываются все вопросы - Скринкаст NODE.JS.

В свете этого скринкаста в обеих статьях обнаруживаются достаточно существенные ошибки. Прямо и не знаю - может убрать обе эти статьи о греха подальше? ))

CMUS - изменить тему оформления

После того, как [CMUS][1] успешно установлен, хорошо было бы - изменить тему оформления - с той, которая есть по умолчанию.Это легко сдел. … Continue reading

ES2015 (ES6) поставляется со встроенной поддержкой модулей в JavaScript. Как и в CommonJS, здесь каждый файл представляет собой отдельный модуль. Чтобы объекты, функции, классы или переменные стали доступными в сети, достаточно просто экспортировать их, а затем импортировать туда, где они необходимы, в другие файлы. Angular 2 интенсивно использует модули ES6, поэтому тем, кто работал с Angular, синтаксис будет хорошо знаком. В целом синтаксис довольно прост.

Экспорт

Вы можете экспортировать элементы по одному. То, что вы не экспортировали, не будет доступно за пределами модуля.

export const myNumbers = [1, 2, 3, 4];
const animals = ['Panda', 'Bear', 'Eagle']; // Not available directly outside the module
export function myLogger() console.log(myNumbers, animals);
>
export class Alligator constructor() // .
>
>

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

Экспорт с псевдонимом

Вы также можете присвоить экспортированным элементам псевдонимы. Это делается с помощью ключевого слова as:

Экспорт элемента по умолчанию

Вы можете определить экспорт по умолчанию с помощью ключевого слова default:

export const myNumbers = [1, 2, 3, 4];
const animals = ['Panda', 'Bear', 'Eagle'];
export default function myLogger() console.log(myNumbers, pets);
>
export class Alligator constructor() // .
>
>

Импорт

Синтаксис импорта также очень прост: в нем используется ключевое слово import, элементы, которые нужно импортировать, в фигурных скобках, а затем расположение модуля относительно текущего файла:

Импорт с псевдонимом

Вы также можете использовать псевдонимы элементов в импорте:

import myLogger as Logger from 'app.js';

Импорт всех экспортированных элементов

Вы можете импортировать все, что ранее было экспортирован, следующим образом:

import * as Utils from 'app.js';

Это позволяет получить доступ к элементам с точечной нотацией:

Импорт модуля с элементом по умолчанию

При импорте элемента по умолчанию вы присваиваете ему имя по вашему выбору. В следующем примере Logger – это имя, данное импортированному элементу по умолчанию:

import Logger from 'app.js';

А вот так можно импортировать нестандартные элементы вместе элементом по умолчанию:

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

Предыстория возникновения import type и export type¶

Представьте сценарий по которому существуют два модуля включающих экспорт класса. Один из этих классов использует другой в аннотации типа своего единственного параметра конструктора, что требует от модуля в котором он реализован, импорта другого модуля. Подвох заключается в том, что несмотря на использование класса в качестве типа, модуль в котором он определен вместе с его содержимом, все равно будет скомпилированно в конечную сборку.

Поскольку при использовании допустимых JavaScript конструкций исключительно в качестве типа, было бы разумно ожидать, что конечная сборка не будет обременена модулями в которых они определены. Кроме того конструкции присущие только TypeScript, хотя и не попадают в конечную сборку, в отличие от модулей в которых они определенны. Если в нашем примере поменять тип конструкции SecondLevel с класса на интерфейс, то модуль ./FirstLevel.js все равно будет содержать импорт модуля ./SecondLevel.js содержащего экспорт пустого объекта export <>; . Не лишним будут обратить внимание, что в случае с интерфейсом, определяющий его модуль мог содержать и другие конструкции. И если бы среди этих конструкций оказались допустимые с точки зрения JavaScript, то они, на основании изложенного ранее, попали бы в конечную сборку. Даже если бы вообще не использовались.

Это поведение привело к тому, что в TypeScript появился механизм импорта и экспорта только типа. Этот механизм позволяет устранить рассмотренные случаи, тем не менее имеет несколько нюансов, которые будут подробно изложены далее.

import type и export type - форма объявления¶

Форма уточняющего импорта и экспорта только типа включает в себя ключевое слово type идущее следом за ключевым словом import либо export .

Ключевое слово type можно размещать в выражениях импорта, экспорта, а также ре-экспорта.

Единственное отличие импорта и экспорта только типа от обычных одноименных инструкций состоит в невозможности импортировать в одной форме обычный импорт\экспорт и по умолчанию.

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

Импорт и экспорт только типа на практике¶

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

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

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

Вспомогательный флаг --importsNotUsedAsValues¶

Другая задача, решаемая с помощью данного, заключается в управлении включением модулей в конечную сборку при помощи вспомогательного флага --importsNotUsedAsValues , значение которого может принадлежать к одному из трех вариантов. Но прежде чем познакомится с каждым из них, необходимо поглубже погрузится в природу возникновения необходимости в данном механизме.

Большинство разработчиков пользуясь механизмом импорта\экспорта в повседневной работе даже не подозревают что с ним связанно немало различных трудностей, возникающих из-за механизмов призванных оптимизировать код. Но для начала рассмотрим несколько простых вводных примеров.

Представьте ситуацию при которой один модуль импортирует необходимый ему тип представленный интерфейсом.

Поскольку интерфейс является конструкцией присущей исключительно TypeScript, то неудивительно что после компиляции от неё и модуля в которой она определена не останется и следа.

Теперь представьте что один модуль импортирует конструкцию представленную классом, который задействован в логике уже знакомой нам функции action() .

В этом случае класс Person был включён в скомпилированный файл поскольку необходим для правильного выполнения программы.

А теперь представьте ситуацию когда класс Person задействован в том же модуле action.ts , но исключительно в качестве типа. Другими словами он не задействован в логике работы модуля.

Подумайте, что должна включать в себя итоговая сборка? Если вы выбрали вариант идентичный первому, то вы совершенно правы! Поскольку класс Person используется в качестве типа то нет смысла включать его в результирующий файл.

Подобное поведение кажется логичным и возможно благодаря механизму называемому import elision. Этот механизм определяет что конструкции которые теоретически могут быть включены в скомпилированный модуль требуются ему исключительно в качестве типа. И как уже можно было догадаться именно с этим механизмом и связанны моменты мешающие оптимизации кода.

Рассмотрим пример состоящий из двух модуле. Первый модуль экспортирует объявленные в нем интерфейс и функцию использующая этот интерфейс в аннотации типа своего единственного параметра. Второй модуль лишь ре-экспортирует интерфейс и функцию из первого модуля.

Поскольку компиляторы как TypeScript, так и Babel неспособны определить является ли конструкция IActionParams допустимой для JavaScript в контексте файла, существует вероятность возникновения ошибки. Простыми словами, механизмы обоих компиляторов не знают нужно ли удалять следы связанные с IActionParams из скомпилированного JavaScript кода или нет. Именно поэтому существует флаг --isolatedModules активация которого заставляет компилятор предупреждать об опасности данной ситуации.

Механизм уточнения способен разрешить возникающие перед import-elision трудности ре-экспорта модулей, предотвращению которых способствует активация флага --isolatedModules .

Рассмотренный выше случай можно разрешить с помощью явного уточнения формы импорта\экспорта.

Специально введенный и ранее упомянутый флаг --importsNotUsedAsValues ожидает одно из трех возможных на данный момент значений - remove , preserve или error .

Значение remove реализует поведение по умолчанию и которое обсуждалось на протяжении всей главы.

Значения preserve способно разрешить проблему возникающую при экспорте так называемых сайд-эффектов.

Несмотря на то что модуль module-with-side-effects.ts задействован в коде, его содержимое не будет включено в скомпилированную программу, поскольку компилятор исключает импорты конструкций не участвующих в её логике. Таким образом функция incrementVisitCounterLocalStorage() никогда не будет вызвана, а значит программа не будет работать корректно!

Решение этой проблемы заключается в повторном указании импорта всего модуля. Но не всем такое решение кажется очевидным.

Теперь программа выполнится так как и ожидалось. То есть модуль module-with-side-effects.ts включен в её состав.

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

Также флаг preserve в отсутствие уточнения поможет избавиться от повторного указания импорта. Простыми словами значение preserve указывает компилятору импортировать все модули полностью.

Несмотря на то что импортировались исключительно конструкции-типы, как и предполагалось, модули были импортированы целиком.

В случае уточнения поведение при компиляции останется преждним. То есть импорты в скомпилированный файл включены не будут.

Импорты модулей будут отсутствовать.

Если флаг --importsNotUsedAsValues имеет значение error , то при импортировании типов без явного уточнения будет считаться ошибочным поведением.

Скомпилированный код после устранения ошибок, то есть после уточнения, включать в себя импорты не будет.

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

Почему я перестал использовать экспорт по умолчанию в моих JavaScript-модулях

После нескольких лет борьбы с экспортом по умолчанию (export default), я переосмыслил свои подходы.

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

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

Надеюсь, что этих уточнений достаточно, чтобы избежать путаницу в оставшейся части поста.

Насколько мне известно, дефолтные экспорты были популяризованы в CommonJS, где модуль может экспортировать дефолтное значение, например

Этот код экспортирует класс LinkedList но не указывает имя, которое будет использовано пользователем модуля. Предпологая, что имя файла linked-list.js , дефолтное значение можно импортировать в другой CommonJS модуль вот так:

Функция require() возвращает значение, которое я назвал LinkedList , чтобы оно совпадало с тем, что находится в файле linked-list.js , но я мог и назвать его foo или Mountain , или любой другой идентификатор.

Популярность дефолтных экспортов в CommonJS означала что модули в JavaScript были разработаны с целью поддержки этого шаблона:

ES6 поддерживает стиль одиночного/дефолтного экспорта и предоставляет приятный синтаксис для экспорта по умолчанию.

— Дэвид Херман (David Herman) 19 июня 2014

Поэтому, в JavaScript-модулях можно использовать дефолтный экспорт:

А затем, импортировать:

Еще раз, имя переменной LinkedList — это произвольный выбор пользователя (если он не аргументированный), оно может быть как Dog , так и simphony .

CommonJS и JavaScript-модули, в добавок к дефолтным экспортам, поддерживают и именованные экспорты.

В CommonJS именованный экспорт создается при помощи добавления имени к объекту экспорта, например так:

Затем можно импортировать в другой файл:

Это значит, что имя, которое я использовал с ключевым словом const может быть каким угодно, но я назвал его так, чтобы оно совпадало с экспортируемым именем LinkedList .

В JavaScript модулях, именованный экспорт выглядит так:

В этом коде, LinkedList не может быть случайно выбранным идентификатором и должен совпадать с именованным экспортом LinkedList . Это и есть существенное отличие от CommonJS.

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

Прежде чем читать дальше, будет полезно узнать некоторые мои предпочтения при написании кода. Это основные принципы, которые я применяю к своему коду, несмотря на используемый язык программирования:

Примечание

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

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

Как я и отметил в своем твите, мне было сложно определить что я импортирую из модуля в котором используется дефолтный экспорт. При работе с незнакомым модулем или файлом может быть сложно определить, чем является импорт, например:

  • Если я автор list.js , то я могу открыть файл и посмотреть его экспорт.
  • Если автором list.js являюсь не я, то я могу почитать документацию.

Некоторые скажут, что IDE решает все проблемы, что IDE должны быть достаточно умными, чтобы узнать, что импортируется. Хотя я и за умные IDE в помощь разработчикам, но я считаю, что требовать эффективного использования особенностей языка проблематично.

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

Импорт именованного экспорта означает, что по крайней мере ссылка на каноническое имя будет везде, где он используется. Даже если вы решите переименовать импорт, решение будет явным, и не может быть принято без ссылки на каноническое имя каким-либо образом. Например, в CommonJS:

В JavaScript модулях:

В обоих видах модулей, придется явно указать, что LinkedList теперь будет называться MyList

Когда именование постоянно во всей кодовой базе, легко делать такие вещи, как:

  1. Поиск по кодовой базе с целью найти информацию об использовании
  2. Рефакторинг имени чего-либо во всей кодовой базе

Можно ли делать это, используя дефолтные экспорты и произвольные имена? Я думаю, да, но я также думаю, что это будет намного сложнее и будет больше ошибок.

Именованные экспорты в JavaScript-модулях имеют значимое преимущество перед дефолтными экспортами в том, что ошибка будет видна при попытке импорта того, чего не существует в модуле. Рассмотрим код:

Если LinkedList не существует в list.js , то ошибка будет видна. Кроме того, инструменты как IDE и ESLint1 могут с легкостью обнаружить отсутствующую ссылку еще до исполнения кода.

Если говорить об IDE, WebStorm может писать import за вас.2. При написании идентификатора, который не был объявлен в файле, WebStorm выполнит поиск модулей в проекте, чтобы определить является ли идентификатор именованным экспортом в другом файле. В этом случае WebStorm сделает одно из двух действий:

  1. Подчеркнет идентификатор, у которого нет определения и покажет import , который пофиксит это исправит.
  2. Автоматическое добавление правильного import 'а (если у вас включены автоматические импорты) может автоматически добавить import на основе введенного идентификатора.

Существует плагин для Visual Studio Code3, который предоставляет аналогичный функционал. Такой функционал невозможен при использовании дефолтных экспортов, потому что не существует канонического имени для вещей, которые вы хотите импортировать.

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

Как и ранее, я напоминаю, что это всего лишь моё мнение и вы можете не посчитать мои доводы убедительными. Эта запись не предназначалась для того, чтобы убедить кого-либо прекратить использовать дефолтные экспорты, а скорее для того, чтобы объяснить тем, кто заинтересовался, почему я перестану использовать дефолтные экспорты в своих модулях.

Если этот пост показался полезным, то вы можете поддержать автора:

Читайте также: