Я не знаю TypeScript, поэтому и пишу эту статью. У меня есть некоторый опыт программирования на Java и PHP и этот опыт заставляет меня кодировать на JavaScript'е соответствующим образом. К последней моей статье коммент от коллеги Silverthorne был такой:
export default class TeqFw_Http2_Back_Server { constructor(spec) { // EXTRACT DEPS /** @type {Function|TeqFw_Http2_Back_Server_Stream.action} */ const process = spec['TeqFw_Http2_Back_Server_Stream$']; /** @type {TeqFw_Web_Back_Handler_Registry} */ const registryHndl = spec['TeqFw_Web_Back_Handler_Registry$'];
зачем все это, когда есть TypeScript?
В ответном комменте я попросил от него продемонстрировать TS-код, который делает то же самое. Он не ответил. Я добавил коммент с просьбой, чтобы кто-угодно продемонстрировал TS-код, который делает то же самое. Ничего. И вот я пишу уже статью с аналогичной просьбой.
Код в примере — это рабочий код на JS. И он написан в стиле, который сформировался у меня за время работы с Java и PHP. Я привык, что я могу разбивать код своего проекта на пакеты. Что я могу размещать пакеты в отдельных git-репозиториях. Что maven и composer помогают мне собирать проект из пакетов. Что я могу использовать одни и те же пакеты в разных проектах. Что в рамках одного проекта код из одного пакета может свободно использовать код из другого пакета.
Что-то подобного я ожидал и от JavaScript'а после того, как в нём появились nodejs (2009) и npm (2010). Так как JavaScript преимущественно использовался в браузерах, то пакеты начали добавлять в проекты (сначала вручную, затем через системы сборки — grunt и webpack c 2012-го, gulp с 2013-го), а в браузер загружали код при помощи requirejs (2009) и browserify (2011). Появились различные форматы загружаемых модулей (скриптов) — AMD, CommonJS, UMD. В 2012-м даже появились типы, пусть и с транспиляцией — TypeScript. Типы без транспиляции появились в 2015-м (EcmaScript 6). В общем, всё указывало на то, что JS стал "взрослым" языком.
Для меня, после почти десятка лет программирования на PHP, привычен такой код конструктора:
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Flancer32\BotSess\Service\Clean\Files $servClean
) {...}
Это нормально, когда я указываю в конструкторе просто тип зависимости, а среда выполнения сама определяет местоположение файла с исходником, подгружает соответствующий код и создаёт требуемую зависимость. Без всяких include
, import
, require
— я ведь уже указал тип зависимости.
А вот это, с моей точки зрения, ненормально:
import UserController from './controllers/UserController';
...
const userController = Container.get(UserController);
и это тоже:
import UserService from "../services/UserService";
class UserController {
constructor(private readonly userService: UserService) {}
}
Я в коде дважды определяю, какая зависимость мне нужна — при импорте класса и в контейнере (конструкторе). В PHP так писали до 2004-го года (__autoload), а в Java так не писали вообще никогда (Java'вский import
ничего не подгружает, а работает как alias, можно и без import
'ов, если указывать полные имена классов).
Более того, в пределах одного npm-пакета я завязан на файловую структуру (import ... from './.../...';
), а при межпакетном взаимодействии я должен в package.json
пакета-донора прописывать экспорты до соответствующего файла.
"main": "index.js",
"exports": {
".": "./index.js",
"./promise": "./promise.js",
"./promise.js": "./promise.js"
}
Согласен, что в java-package & php-namespace мы привязаны к структуре логического разбиения кода:
com.teqfw.http2.back.server.Stream
\TeqFw\Http2\Back\Server\Stream
Тем не менее, у нас остаётся некоторая свобода внутри этой структуры — при росте пакета com.teqfw.http2
мы можем выделить из него пакет com.teqfw.http2.back
в отдельный пакет без изменения зависимого кода. Мне не нужно будет совмещать export
пакета-донора с import
пакетов-реципиентов. В Java мне вообще ничего не надо совмещать, а в PHP нужно будет прописать соответствующий маппинг в composer.json
нового пакета, а всё остальное сделают composer
и autoloader
.
Когда я в очередной раз решил "прокачать" свои навыки в JS пару лет назад, я сразу же стал искать подходящий DI-контейнер. И я очень удивился, когда столкнулся с необходимостью импортировать исходники самостоятельно. Даже TypeScript'овые библиотеки не давали мне желаемого функционала. А мои желания поначалу были совсем простые — DI-контейнер, одинаково работающий в браузере и в nodejs. Лучшее, что я нашёл — awilix, который работал только для nodejs.
Мне пришлось самому разбираться, в чём отличия работы функции import
в браузере и в nodejs. И, разумеется, я делал это на ванильном JS — зачем мне "прокладка" в виде TS, тем более, что любой код на JS также является и валидным кодом на TS?
Я не знаю TS, и не сильно удивлюсь, если кто-то без import
'ов напишет пример конструктора, использующего в качестве зависимости скрипт из другого npm-пакета. Но мне будет крайне любопытно изучить библиотеку, которая это делает, и разобраться, как она это делает. Особенно, если эта библиотека работает и в браузере, и в nodejs. Плюсик в карму обещаю (если моей будет достаточно для этого к тому моменту). А возможно это даже станет поводом перейти на TypeScript и "не мучаться", как рекомендовали некоторые коллеги.
Постмортем
Ну что ж, в целом получилось неплохо. Моей кармы хватает ещё на 6 таких статей :)
Действительно, в JS нет autoloader'а, как нет его и в TS. В nodejs
устоялась практика export
'ов-import
'ов на уровне пакетов (маппинг логической структуры export
'ов на файловую структуру es-модулей). Для браузеров средства типа webpack
'а собирают весь фронт приложения в единый массив (или несколько массивов — бандлов).
Межпакетные import
'ы преимущественно используются в nodejs
, для браузеров пакеты помещаются в бандлы (файлы). Внутри такого файла import
'ы становятся излишними. Необходимость tree shaking как раз и является следствием такого подхода — когда весь пакет помещается в бандл. Затем из него приходится удалять код, который в данном бандле не используется. Причём оптимизация происходит на уровне export
'ов. Т.е., если в каком-то npm
-пакете нужно обеспечить последующую оптимизацию кода на уровне отдельного es-модуля, то в в его package.json
нужно прописать export
для каждого такого es-модуля (как правило, модули внутри пакета связаны по цепочкам и достаточно в экспорте прописать головной модуль цепочки, т.е., в пакете может быть 100 es-модулей и только 10 экспортов).
Таким образом, на данный момент, в JS-приложении, состоящем из множества npm-пакетов, "кирпичом" является не es-модуль (отдельный файл), а export
'ы отдельного пакета (точки входа в цепочки связанных es-модулей). В nodejs
для загрузки кода из сторонних пакетов используется (или пока ещё только предполагается использовать — в описании нет ключа exports
?) autoload'инг на уровне логической структуры export'ов (@scope/package/path/to/export
), для браузеров autoload'инг не нужен, но бандлы также собираются "с шагом в один export
".
Такого я точно не видел ни в Java, ни в PHP. В них отдельные файлы пакета доступны для сторонних пакетов без ограничений. В чём-то это преимущество, в чём-то нет. Продолжаю вести наблюдение...