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

До модулей (Темные века)

Когда-то, до открытия огня, когда планетой правили динозавры, разработчикам javascript пришлось пройти через ад, чтобы заставить их код работать в браузере. Им приходится вручную импортировать кучу js-файлов через HTML-тег <script> вот так

<!DOCTYPE html>
<script src="fileOne.js"></script><html>
<script src="fileTwo.js"></script>
<body>
</body>
</html>

Теперь было много проблем, связанных с этим подходом

  • Был другой HTTP-запрос для получения каждого файла js, что повлияло на производительность сайта.
  • Файлы были загружены последовательно, поэтому, если функция или объект были объявлены в fileTwo.js и упомянуты в fileOne.js, тогда браузер выдаст ошибку, например:
// fileOne.js
console.log(sum(2, 4));
// fileTwo.js
function sum(x, y) {
  return x + y;
}

Браузер выдает ошибку

fileTwo.js:1 Uncaught ReferenceError: sum is not defined
    at fileTwo.js:1
(anonymous) @ fileTwo.js:1

и если мы просто поменяем порядок, в котором объявлены <script> теги, мы получим красивый вывод на консоли

6
  • Функции из одного файла могут перезаписывать функции из другого, если они неправильно импортированы или управляются неправильно.
  • Файл не загружается до тех пор, пока не будут загружены его предыдущие файлы.
// fileOne.js
let len = 1000000000;
while (true) len--;
// fileTwo.js
console.log("File Two");

Мы можем изменить значение len и увидеть задержку печати журнала File Two.

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

Модули доходов (Открытие огня)

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

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

Но не было единого стандарта модуля, который был бы принят для достижения этой цели, как на стороне сервера было CommonJs

CommonJs

CommonJS оборачивает каждый модуль в функцию с именем «require» и включает объект с именем module.exports, который экспортирует код для обеспечения доступности, который требуется другим модулям. Все, что вам нужно сделать, это добавить все, что вам нужно для доступа к другим файлам, в exportsobject и потребовать модуль в зависимом файле.

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

// index.js
console.log("index loaded started")
require('./child.js')()
console.log("index loading finished")
// child.js
module.exports = function() {
    console.log("Child loaded")
}

при запуске с командой node index.js выдает следующий вывод

index loaded started
Child loaded
index loading finished

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

RequireJs

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

define(['module1', ',module2'], function(module1, module2) {
  console.log(module1.setName());
});

Функция вызывается только после завершения загрузки запрошенных модулей.

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

Вуаля, представляем модули ES6

Стандарт модулей ES6 привнес изоморфный javascript в картину. С ES6 мы можем импортировать и экспортировать модули как в синхронном, так и в асинхронном режимах работы. Таким образом, их можно использовать как на стороне клиента, так и на стороне сервера, тем самым устраняя разрыв между требованиями к набору навыков для разработчиков интерфейсов и серверов.

Оператор require используется для включения модулей в пространство имен. Он не является динамическим, его нельзя использовать в любом месте файла. Это отличается от requireи define. exportstatement делает элементы общедоступными. Это статичное поведение заставляет статические анализаторы строить дерево зависимостей при связывании файла без запуска кода. Из-за чего модули ES6 предварительно анализируются для разрешения дальнейшего импорта до выполнения кода, в то время как модули CommonJS загружают зависимости по требованию во время выполнения кода. Давайте разберемся в этом на нескольких примерах:

console.log("Index Started")
const importedVar = require('./child.js')
console.log("Index Ended")
 // index.js
console.log("Child Loaded")
module.exports = "fsdfsfsdf"
// child.js

Приведенный выше код использует модули commonJs и при выполнении дает следующий результат.

Index Started
Child Loaded
Index Ended

а с другой стороны, если та же логика написана с использованием модулей ES6

console.log("Index Started")
import importedVar from './child.mjs';
console.log("Index Ended")
 // index.js
console.log("Child Loaded")
export default "fsdfsfsdf"
// child.js

вывод

Child Loaded
Index Started
Index Ended

Приведенные выше примеры показывают, что при использовании модулей CommonJS файл child.js загружался во время выполнения только тогда, когда это требовалось, тогда как в случае модулей ES6 файл предварительно анализировался перед выполнением index.js.

Из-за этого мы не можем использовать оба типа форматов модулей в одном файле. Файл JS будет использовать либо модули ES6, либо CommonJS/RequireJS.

Использование модулей ES6 на стороне сервера

В NodeJS добавлена ​​экспериментальная поддержка модулей ES6, так как он не может сделать жесткий переход, поскольку модулям ES6 может потребоваться больше времени для созревания, а разработчикам и их кодовым базам потребуется время для выполнения этой миграции. Кроме того, модули ES6 не смогут выполняться в очень старых версиях Node.

Чтобы запустить модули ES6 в Node версии ≥ 13, вам нужно либо

  • Сохраните файл с расширением .mjs или
  • Добавьте { "type": "module" } к ближайшему package.json.

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

console.log("Index Started");
import importedVar from "./child.js";
console.log("Index Ended");
// index.mjs
console.log("Child Loaded")
module.exports = "fsdfsfsdf"
// child.js

дает вывод

Child Loaded
Index Started
Index Ended

Если мы упомянем { type: "module" } в package.json, мы все еще можем использовать CommonJs, для этого файлы с модулями CommonJS должны иметь расширение .cjs Например:

console.log("Index Started");
import importedVar from "./child.js";
console.log("Index Ended");
// index.js
console.log("Child Loaded")
module.exports = "fsdfsfsdf"
// child.cjs

С { type: "module" } в package.json дает тот же результат, что и выше.

В случае Node версии 8–12 для запуска модулей ES6 используйте расширение .mjs и запустите как

node --experimental-modules index.mjs

Использование модулей ES6 на стороне клиента

Большинство современных браузеров, таких как Safari, Chrome, Firefox и Edge, теперь поддерживают модули ES6, но некоторые из их очень старых версий могут не поддерживаться. Чтобы использовать модули ES6 в браузере, поддерживающем модули ES6, вы можете добавить type="module" в тег <script>.

<script type="module" src="./main.js"></script>

или добавить встроенную логику

<script type="module">
  import { something } from './somewhere.js';
  // ...
</script>

Но есть простые решения, позволяющие заставить ваш код js работать во всех версиях браузера. Этого можно добиться с помощью транспиляторов, таких как Babel, Browserify и т. д., которые мы рассмотрим в последующих историях.

Вывод

Современный javascript, который мы используем изо дня в день, является побочным продуктом многолетней напряженной работы тысяч разработчиков по всему миру. Модули ES6 представляют собой звезду в галактике изменений, через которые прошел javascript. Чтение и понимание этих изменений дает нам представление о том, как далеко мы продвинулись и как небо является пределом того, что готовит будущее.

использованная литература