Представьте себе: у вас есть большой многомодульный проект ClojureScript, и вы планируете выполнить новое развертывание в рабочей среде. В вашем проекте используется режим расширенной оптимизации компилятора CLJS. Кажется, все работает гладко. Вы выполняете последние E2E-тесты перед выпуском.

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

БАМ! Вы получаете пощечину примерно такой ошибкой:

Uncaught Error: No protocol method IMultiFn.-add-method defined for type function: function XR() { [native code] }
    at Nb ((index):964)
    at yh ((index):443)
    at (index):236
(index):143 Uncaught TypeError: Cannot read property 'j' of undefined
    at NU ((index):143)
    at login-a5a67c0fffdfa158b7220c8c2553253b645e4e2e.js:169

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

Кроме того, при использовании Firefox все работало нормально, но теперь скомпилированный код дает сбой в Google Chrome. Что здесь происходит? Крайний срок выпуска приближается, и вы должны исправить это как можно скорее.

Когда вы создаете свой код с помощью компилятора CLJS, он генерирует код JavaScript, совместимый с уровнем advanced_optimizations компилятора Google Closure. Затем, если включена опция компилятора :optimizations :advanced, компилятор CLJS уменьшит результат с помощью Advanced Optimizations Closure Compiler.

Режим расширенной оптимизации Closure Compiler может вызвать некоторые ошибки в вашей сборке, которые может быть неудобно отслеживать, если вы новичок в Closure Compiler. Кроме того, помощь может быть трудно найти, если вы пытаетесь выполнить поиск в Интернете с сообщением об ошибке, потому что все имена функций искажены.

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

Как избежать проблем с расширенной компиляцией

Для ваших производственных сборок рекомендуется использовать параметр компилятора :optimizations :advanced CLJS. Вы значительно уменьшите конечный размер сборки, используя более агрессивные расширенные преобразования, такие как удаление мертвого кода и агрессивное переименование.
Подробнее о расширенных преобразованиях читайте здесь: Уровни компиляции Closure Compiler

Различные инструменты сборки, такие как lein-cljsbuild и shadow-cljs, могут передавать компилятору Closure разные параметры по умолчанию. Я не буду рассказывать о различиях в этом сообщении в блоге, а вместо этого дам несколько общих рекомендаций о том, как избежать проблем с расширенной компиляцией в вашем проекте.

Держите своих экстернов в хорошей форме

Extern — это механизм для объявления имен, которые не должны быть изменены (переименованы) Closure Compiler. Когда вы используете внешние библиотеки, убедитесь, что они поставляются в комплекте с внешними, если вы используете инструмент сборки, такой как lein-cljsbuild. Или предоставьте свои собственные файлы externs, если это необходимо. В противном случае Closure Compiler будет непреднамеренно искажать ссылки на внешние символы во время расширенной компиляции, вызывая ошибки, обнаруживаемые только позже при запуске скомпилированного кода.

Чтобы добавить свои собственные внешние компоненты, используйте параметр компилятора, например: :externs ["externs.js"], и предоставьте файл externs.js в своем рабочем каталоге. Кроме того, хорошо использовать опцию компилятора :infer-externs true. Эта опция позволит автоматически генерировать внешние вызовы для вызовов взаимодействия JavaScript.

Стоит знать, что Closure Compiler включает внешние модули для стабильных API JavaScript, но новые функции, имеющие экспериментальный статус, могут быть еще не включены. Экспериментальные API меняются очень быстро, поэтому нет смысла включать их в инструмент Closure Compiler. Итак, если вы планируете использовать некоторые экспериментальные функции, убедитесь, что вы добавили их в свой собственный файл externs.

Подробнее об использовании внешних библиотек JavaScript читайте здесь

Разделение кода может привести к конфликту имен

Если вы разбиваете свой код CLJS на :modules, вы можете использовать опцию компилятора :rename-prefix "...".

Разделенные модули работают в глобальной области действия JavaScript, поэтому они могут мешать другому коду, загруженному на той же странице (например, Google Analytics), и вызывать непредсказуемые ошибки, если возникают конфликты имен. При использовании :rename-prefix в качестве префикса лучше всего использовать очень короткую строку, например: :rename-prefix "r_"

Это немного увеличит окончательный (сжатый gzip) размер сборки, потому что теперь каждая сжатая глобальная переменная будет иметь префикс «r_».

Оберните выходные данные CLJS, чтобы предотвратить загрязнение глобальной области видимости

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

Если вы загружаете другой код на ту же страницу и не разбиваете код на несколько модулей, вы можете использовать параметр компилятора :output-wrapper true. Компилятор обернет полученный код JavaScript файлом default(function(){...​};)(). Это предотвратит загрязнение глобальной области видимости JavaScript и, таким образом, предотвратит конфликты с другим внешним кодом.

Однако, если в вашем проекте несколько модулей, вы должны добавить параметр компилятора :rename-prefix-namespace "...". Это позволяет каждому модулю получить доступ к переменным, определенным в других модулях, от которых он зависит. Это работает аналогично опции :rename-prefix. Разница в том, что каждая глобальная переменная теперь будет относиться к одной глобальной переменной, а не ко многим. Например, если вы используете префикс-пространство имен, подобное этому: :rename-prefix-namespace "P", скомпилированный код будет ссылаться на переменную foo, например p.foo. Эта опция также увеличит окончательный размер сборки, как и опция :rename-prefix, поэтому подумайте, прежде чем ее использовать.

Существуют и другие интересные параметры компилятора, не связанные напрямую с расширенными оптимизациями, например :fn-invoke-direct, которые могут быть полезны для оптимизации кода, критичного для производительности. Подробнее о них можно прочитать в документации CLJS.

Отладка ошибок расширенной компиляции

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

Добавьте две дополнительные опции компилятора :pseudo-names true и :pretty-print true для сборки Advanced Compilation. Теперь ваша ошибка будет отображать удобочитаемое имя, соответствующее имени в исходном коде. Это поможет вам определить, отсутствует ли внешнее определение.

Кроме того, если ваша ошибка исчезнет после включения вышеупомянутых параметров компилятора, это даст вам подсказку о возможной проблеме конфликта имен. Когда возникает проблема с конфликтом имени измененной переменной с именем внешней переменной, проблема исчезает при изменении имени измененной переменной, как это происходит при включении опции :pseudo-names.

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

Иногда может случиться что-то фанк

В некоторых редких случаях могут возникнуть сложные проблемы компиляции, которых не так просто избежать. Вернемся к началу. У нас возникла ошибка при запуске нашего встроенного веб-приложения в Chrome. У Firefox не было проблем.

Uncaught Error: No protocol method IMultiFn.-add-method defined 
                for type function: function XR() { [native code] }
    at Nb ((index):964)
    at yh ((index):443)
    at (index):236

Трассировка стека указывала на определенный мультиметод в исходном коде CLJS.

На первый взгляд ошибка может не иметь смысла. Скомпилированный код пытается вызвать метод протокола IMultiFn -add-method для функции с именем XR. При поиске испорченного XR в выводе сборки вроде бы все в порядке. XR выглядит хорошо и должно работать, верно?

Главное, на что следует обратить внимание в сообщении об ошибке, — это "[native code]" часть "function XR() { [native code] }". Это говорит нам о том, что скомпилированный код пытается вызвать родную функцию браузера XR вместо нашей испорченной XR. Случайно компилятор Closure назвал наш мультиметод XR, который сталкивается с предоставленной браузером функцией XR.

Иногда вы можете столкнуться с такими ошибками при использовании более старой версии Closure Compiler. Разработчики постоянно добавляют новые функции в веб-браузеры. Когда пользователь обновляет свой веб-браузер, есть вероятность, что было добавлено новое зарезервированное слово, которое сталкивается с измененной переменной.

Оказывается, XR — это зарезервированное слово, добавленное API платформы WebXR. Когда произошла ошибка, XR был добавлен в новейшую версию Google Chrome, но еще не в Firefox. Новые версии Closure Compiler учитывают это, предоставляя для него внешний модуль. Не всегда легко обновить версию ClojureScript и, следовательно, компилятор Closure, используемый в проекте.

В этом случае вы можете быстро решить проблему, добавив собственный внешний вид:

// externs.js

var XR = {};

Вывод

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

Первоначально опубликовано на https://dev.solita.fi 25 июня 2020 г.