Как стать автором
Обновить

Почему я «мучаюсь» с JS

Время на прочтение5 мин
Количество просмотров14K

Я не знаю 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. В них отдельные файлы пакета доступны для сторонних пакетов без ограничений. В чём-то это преимущество, в чём-то нет. Продолжаю вести наблюдение...

Теги:
Хабы:
Всего голосов 45: ↑12 и ↓33-17
Комментарии109

Публикации

Истории

Работа

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань