Модули и проект: import, export и структура папок
- Зачем это нужно
- Файл закрыт, пока не сказано обратное
- Две системы модулей
- Порядок выполнения не совпадает с порядком строк
- Расширение в пути: правильного варианта нет
- Циклическая зависимость
- Импорт только типов
- Папка с пакетами и описание проекта
- Как разложить решение по папкам
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Экзамен по блоку «TypeScript»
- Куда дальше: проверенные ресурсы
Зачем это нужно
Пока в решении три функции, всё живёт в одном файле. Как только появляются развёртывание контрактов, служба мониторинга позиций, тесты и расчёт отношения долга к залогу — один файл превращается в тысячу строк, где ничего не найти.
Разложить по файлам просто. Сложно другое: в этом языке две несовместимые системы подключения файлов, и половина ошибок новичка — про них. Плюс расширение в пути импорта, у которого нет одного правильного варианта. Плюс циклическая зависимость, которая в одной системе падает, а в другой тихо подсовывает пустоту.
Всё это разбирается прогонами — и на этом блок TypeScript заканчивается.
Файл закрыт, пока не сказано обратное
// util.ts
export const LLTV = 80;
export function ltv(debt: number, collateral: number) { return debt / collateral; }
// script.ts
import { LLTV, ltv } from "./util.js";
Есть и вторая форма — экспорт «по умолчанию», когда файл отдаёт наружу одну главную вещь и её имя при получении можно выбрать самому. Она встречается в чужом коде, но в своих проектах чаще держат именованный: так имя одинаково везде и легче искать.
Две системы модулей
Систем подключения файлов две, и они устроены по-разному.
Старая — родная для среды выполнения: подключение выглядит как вызов функции и срабатывает ровно там, где написано.
Современная — стандарт языка: подключение объявляется словом import, и все такие объявления обрабатываются до первой строки кода.
Разница не косметическая. Первая модель берёт один и тот же файл и меняет только описание проекта рядом с ним:
=== один и тот же user.js, три разных package.json ===
type: "commonjs" ПАДАЕТ -> SyntaxError: Cannot use import statement outside a module
поля type нет РАБОТАЕТ -> RATE = 10
type: "module" РАБОТАЕТ -> RATE = 10
=== при "type": "module" расширения решают за себя ===
require ESM из .cjs РАБОТАЕТ -> RATE = 10
require ESM с await наверху ПАДАЕТ -> ERR_REQUIRE_ASYNC_MODULE
require внутри .mjs ПАДАЕТ -> require is not defined in ES module scope
Главное — первая строка. Ошибку «нельзя использовать import вне модуля» получают почти все, и причина всегда одна: создание проекта по умолчанию прописывает туда старую систему. Лечится одной строкой в описании проекта.
Расширения .mjs и .cjs решают за себя и поле «тип» не спрашивают. Это удобно, когда в одном проекте нужны оба вида файлов.
Старое правило «из старой системы нельзя вызвать современную» в свежих версиях среды больше не действует — за одним исключением: если подключаемый файл ждёт чего-то на верхнем уровне, вызвать его по-старому нельзя, потому что ожидание требует времени, а старая система работает мгновенно.
Порядок выполнения не совпадает с порядком строк
Следствие, о котором стоит знать заранее: в современной системе все подключения обрабатываются до первой строки вашего файла. Печать, стоящая выше подключения, выполнится после того, как подключённый файл отработает целиком.
В старой системе подключение срабатывает там, где написано, и строка выше успевает напечататься.
Практически это всплывает при отладке: «почему сообщение из соседнего файла напечаталось раньше моего первого сообщения». Ответ — потому что так устроена загрузка.
Расширение в пути: правильного варианта нет
Вторая модель проверяет три написания одного и того же импорта — под прямым запуском и под сборкой:
import './util' | node: ERR_MODULE_NOT_FOUND: Cannot find module 'util'
| tsc: TS2835: Relative import paths need explicit file extensions…
Did you mean './util.js'?
import './util.ts' | node: печатает LLTV = 80
| tsc: TS5097: An import path can only end with a '.ts' extension
when 'allowImportingTsExtensions' is enabled.
import './util.js' | node: ERR_MODULE_NOT_FOUND: Cannot find module 'util.js'
| tsc: чисто
tsc собрал в dist, код возврата 0
в dist/jsExt.js осталось: import { LLTV } from './util.js';
node dist/jsExt.js печатает LLTV = 80
Ни один вариант не годится сразу для обоих способов. Выбирать надо по тому, как запускается решение:
- запускаем файлы напрямую — пишем расширение исходника;
- собираем и запускаем собранное — пишем расширение результата.
Последние строки объясняют, почему второе выглядит странно, но правильно: компилятор не переписывает пути. В собранном файле остаётся ровно то, что вы написали, — и там это уже верно, потому что рядом лежит собранный сосед.
Циклическая зависимость
Два файла, ссылающихся друг на друга, — обычная авария растущего проекта. Третья модель показывает, что бывает:
CommonJS: node a.cjs
1. начал выполняться market
2. начал выполняться vault
3. vault видит market.RATE = undefined
4. market видит vault.LLTV = 80
5. market закончил
Warning: Accessing non-existent property 'RATE' … inside circular dependency
код возврата: 0
ESM, цикл на константах: node a.ts
2. начал выполняться vault
ReferenceError: Cannot access 'RATE' before initialization
код возврата: 1
ESM, тот же цикл на функциях: node fa.ts
vault: rate() = 10
market: lltv() = 80
код возврата: 0
В старой системе программа не падает. Сосед видит вместо значения пустоту, доходит до конца с кодом успеха и оставляет только предупреждение. Расчёт при этом пойдёт по пустому значению — и разъедется где-то в отчёте.
В современной падает сразу и громко, с внятным текстом. Это лучше: авария видна там, где она случилась.
На функциях тот же цикл работает — потому что значение берётся в момент вызова, а не в момент загрузки. Знать это полезно, но полагаться на это не стоит.
Лечение одно и то же в обеих системах: общее выносят в третий файл, от которого зависят оба. Циклов после этого не остаётся.
Импорт только типов
Среда, запускающая файлы напрямую, вырезает типы — но не вырезает подключение. Если из соседнего файла берётся только тип, обычный импорт останется строкой загрузки и заставит среду искать файл, в котором после срезания типов не осталось вообще ничего.
Отсюда отдельная форма — «подключить только тип». Она исчезает вместе с типами, и лишней загрузки не происходит.
Папка с пакетами и описание проекта
Папка с установленными пакетами собирается из описания проекта одной командой и целиком из него выводится. В репозиторий её не кладут никогда: даже при одной зависимости там оказываются десятки мегабайт.
Описание проекта делит зависимости на две части: то, без чего решение не работает, и то, что нужно только при разработке — компилятор, тесты. Рядом лежит файл с точными версиями всего дерева; вот он в репозиторий едет обязательно.
Там же живут короткие команды: вместо длинной строки со всеми ключами пишут одно слово.
Как разложить решение по папкам
Типичная структура блокчейн-решения:
| Папка | Что внутри |
|---|---|
contracts/ | исходники контрактов |
scripts/ | скрипты развёртывания и наполнения сети |
test/ | тесты |
src/ | общий код: расчёты, обращения к узлу, типы |
| корень | описание проекта, настройки компилятора, файл исключений |
Правило простое: папка отвечает на вопрос «что это», а не «про что это». Скрипт развёртывания хранилища лежит рядом со скриптом развёртывания маркета, а не рядом с контрактом хранилища.
Отдельно — файл исключений: в него первым делом кладут папку с пакетами и папку сборки.
Соблазн сделать «файл-витрину», который переэкспортирует всё подряд ради коротких импортов, лучше отложить: он моментально создаёт циклы. Витрина безопасна только на самом верху, там, где ниже неё уже никто не импортирует обратно.
Типичные ошибки
«Нельзя использовать import вне модуля». В описании проекта старая система.
Путь без расширения. Среда не найдёт файл, компилятор подскажет нужное написание.
Расширение исходника при сборке. Компилятор откажет отдельной ошибкой.
Расширение результата при прямом запуске. Среда не найдёт файл.
Циклическая зависимость. В одной системе тихая пустота, в другой падение; лечится третьим файлом.
Импорт только типа обычной формой. Останется лишняя загрузка файла, в котором ничего нет.
Папка с пакетами в репозитории. Десятки мегабайт вместо строки в описании.
Файл с точными версиями не в репозитории. У другого человека соберётся другое дерево зависимостей.
Файл-витрина посреди проекта. Быстро создаёт циклы.
Слова главы
- Модуль — файл со своими экспортами; всё остальное снаружи не видно.
export/import— отдать наружу и забрать по имени; есть форма «по умолчанию».- Две системы модулей — старая, родная для среды, и современная, стандарт языка.
- Поле «тип» в описании проекта — решает, как читать файлы
.js;.mjsи.cjsрешают за себя. - Расширение в пути — зависит от способа запуска; компилятор пути не переписывает.
- Циклическая зависимость — тихая пустота или громкое падение; лечится третьим файлом.
- Импорт только типа — исчезает вместе с типами, лишней загрузки не создаёт.
- Папка с пакетами — в репозиторий не кладут; файл точных версий — кладут обязательно.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
export const LLTV = 80;
На чемпионате
Модуль А идёт семь часов, интернета нет, привезти с собой ничего нельзя. Значит, структуру проекта надо уметь поднять с нуля за несколько минут — и не потерять полчаса на ошибку про систему модулей.
Практический порядок первых минут:
- создать описание проекта и сразу дописать в него современную систему модулей;
- проверить, что есть в установленных пакетах: компилятор, библиотека контрактов, инструменты;
- решить, как запускаются скрипты — напрямую или через сборку, — и от этого выбрать расширение в импортах: менять его потом по всему проекту дорого;
- создать папки по назначению и файл исключений.
Отдельно про сдачу: задание требует положить решение в папку с определённым именем и приложить исполняемый файл запуска. Папка с пакетами в это не входит — но если её удалить, решение не запустится без установки. Проще всего оставить всё как есть и написать в инструкции, что и в каком порядке запускать.
Что дальше
Блок TypeScript закончен: запуск, значения, пустота, ветвления, функции, коллекции, свои типы, классы, наследование, ошибки, асинхронность, сеть и модули.
Дальше начинаются инструменты: среда, которая берёт на себя компиляцию контрактов, развёртывание и тесты, и собственный узел сети. Всё, что делалось руками — вызовы, кодирование аргументов, ожидание ответа, — они спрячут за короткими командами. Но теперь понятно, что именно они прячут.
А сейчас — экзамен по блоку.
Углубиться
- Разрешение путей через описание пакета — почему пакет может запрещать импорт своих внутренностей.
- Динамическое подключение — загрузка файла во время работы, а не при старте.
- Псевдонимы путей — короткие имена вместо длинных относительных путей и чем за это платят.
- Сборка в один файл — когда решение удобнее отдать одним файлом и что при этом теряется.
- Диапазоны версий — что означают значки перед номером версии и почему файл точных версий важнее описания.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Экзамен по блоку «TypeScript»
Пятнадцать глав: от первого запуска до модулей. Проверка — по всему блоку.
Сводная проверка по 14 главам блока: 15 вопросов, 10:00 на всё, по одному вопросу, без возврата. Пересдавать можно сколько угодно раз.
Готовность: экзамены глав сданы 0 из 14. Блок можно сдавать и так, но шанс выше после всех глав.
Куда дальше: проверенные ресурсы
- Node.js: модули ECMAScript — современная система, расширения, поле «тип».
- Node.js: CommonJS — старая система и правила совместимости.
- TypeScript: Modules — экспорт, импорт, разрешение путей.
- TypeScript: moduleResolution — почему компилятор требует расширение.
- npm: package.json — зависимости, версии, короткие команды.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.