Почему Вебпак? (или Как не обслуживать Javascript)

Я научился этому на собственном горьком опыте, но, надеюсь, вам не придется.

Еще в начале 2016 года я начал создавать веб-игру под названием GeoArena. Оглядываясь назад, я больше всего сожалею о том, что не использовал Webpack с самого начала.

Когда я основал GeoArena, я был очень новичком в веб-разработке. Никогда раньше не слышав о сборщиках модулей, я вместо этого придумал свои собственные подходы для обслуживания Javascript в Интернете. В этом посте рассматриваются проблемы с этими методами и объясняется, почему следует использовать Webpack вместо этого.

Примечание. Я пытаюсь поощрять использование сборщиков модулей, но не обязательно конкретно Webpack — есть и другие хорошие сборщики, такие как Browserify, Rollup и Parcel.

Примечание № 2. Форматирование этого поста выглядит лучше всего, если его прочитать на victorzhou.com.

Этап 1: Один файл = один скрипт

В самом начале я разделил свой клиентский Javascript на несколько файлов и включил их все в свой HTML с тегами <script>. Вот как раздел скриптов в моем index.html выглядел в первый день сборки GeoArena:

<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script> <script src="https://code.jquery.com/jquery-latest.min.js"></script> <script src="/js/geoarena-networking.js"></script>
<script src="/js/geoarena-game.js"></script>

Отлично работает!, подумал я. Я просто помещу свой сетевой код в geoarena-networking.js, а все остальное — в geoarena-game.js.

Как оказалось, разделение кода всей веб-игры только на два файла на самом деле не работает 🤷. Вот тот же раздел скриптов неделю спустя:

<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.6/socket.io.min.js"></script>
<script src="/geoarena-constants.js"></script>
<script src="/js/geoarena-menu.js"></script>
<script src="/js/geoarena-networking.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.23.0/es6-shim.min.js"></script>
<script src="/js/timesync.min.js"></script>
<script src="/js/geoarena-resources.js"></script>
<script src="/js/Sprite.js"></script>
<script src="/js/Particles.js"></script>
<script src="/InputEvent.js"></script>
<script src="/GameUpdateEvent.js"></script>
<script src="/ObstacleBall.js"></script>
<script src="/Effect.js"></script>
<script src="/Bullet.js"></script>
<script src="/Weapon.js"></script>
<script src="/Ship.js"></script>
<script src="/Neutral.js"></script>
<script src="/js/Minimap.js"></script>
<script src="/js/geoarena-utils.js"></script>
<script src="/js/geoarena-game.js"></script>

Посчитай их. Это 22 сценария. 😬😬

У этого подхода есть несколько больших проблем:

  1. Скорость. Запрос такого большого количества скриптов был узким местом в сети, и в результате мой сайт загружался несколько медленно (имейте в виду, что это было в 2016 году, до эры HTTP/2). . Веб-производительность имеет значение — его важность давно известна и задокументирована. Что вам кажется более эффективным: запрашивать 10 строк кода 100 раз или запрашивать 1000 строк кода один раз?
  2. Область действия. Каждый файл работал в одной и той же глобальной области видимости, поэтому любая объявленная мной переменная была доступна в глобальном объекте окна. Это означало, что все, что я объявлял, должно было иметь уникальное имя — иначе возникла бы коллизия! Вы видите, насколько это проблематично? Больше кода = больше переменных = больше Подождите, я уже использовал это имя раньше?
  3. Зависимости. Мне пришлось вручную поддерживать порядок включения <script>, который удовлетворял моим зависимостям. Например, geoarena-networking.js зависело от socket.io, поэтому мне нужно было убедиться, что включение socket.io появилось над включением geoarena-networking.js. Зависимости нигде явно не объявлялись, но мой порядок <script> должен был удовлетворить их все.

Этап 2: Один большой скрипт

Проблему со скоростью можно решить проще всего: просто поместите все в один огромный файл и загрузите его! Я использовал Gulp, популярный инструмент сборки, чтобы объединить все мои файлы в один гигантский пакет:

gulp.task('build-js', function() {
  return gulp.src([
      './client/js/jquery.min.js',
      './client/js/socket.io.min.js',
      './shared/geoarena-constants.js',
      './client/js/geoarena-menu.js',
      './client/js/geoarena-networking.js',
      // ... more files here
    ])
    .pipe(uglify()) // minify code
    .pipe(concat('geoarena-bundle.min.js'))
    .pipe(gulp.dest('dist'));
});

Все, что мне нужно было сделать, это бежать

$ gulp build-js

и Gulp объединил бы все мои файлы, минимизировал бы их (дополнительное ускорение скорости!) и поместил бы результат в dist/geoarena-bundle.min.js.

Это сократило мой раздел сценариев до одного включения!

<script src="/geoarena-bundle.min.js"></script>

Проблема со скоростью: исправлено ✓.

Этап 3: Немедленно вызываемые функциональные выражения (IIFE)

Я обратился к IIFE, чтобы ограничить объем моих переменных. Каждая функция в Javascript имеет свою область видимости, и любая переменная, объявленная внутри функции, доступна только из этой функции:

let a = 'global scope';

// Here's an IIFE:
(function() {
  let a = 'function scope';
  let b = 'also function scope';
  console.log(a); // "function scope"
  console.log(b); // "also function scope"
})(); // <- immediately invoked

console.log(a); // "global scope"
console.log(b); // ReferenceError: b is not defined

Я обернул весь свой пакет в IIFE, чтобы избежать глобальной области действия, а затем обернул каждый файл индивидуально с помощью IIFE. Любые переменные, к которым мне нужно было получить доступ более чем в одном файле, были объявлены глобально в верхней части моего пакета. Вот примерно как это выглядело:

(function() {
  // Global function fallback
  function gff() {
    console.error('Global function fallback called');
    alert('An unexpected error occurred.');
  }

  // Global variables
  let Constants;
  // ... more variables

  // Global functions
  let playSingleplayer = gff;
  // ... more functions

  // geoarena-constants.js
  (function() {
    Constants = {
      version: "1.0.0",
      // ... more constants
    };
  })();

  // geoarena-menu.js
  (function() {
    playSingleplayer = function() {
      // code
    };
  })();

  // another file
  (function() {
    // Now I can call playSingleplayer()!
    playSingleplayer();
  })();

  // ... more files
})();

Проблема с областью действия: исправлена ✓.

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

Одна из самых интересных ошибок, которые я исправил в свое время, была вызвана этими IIFE — хочу посмотреть, сможете ли вы это обнаружить?

Этап 4: Веб-пакет

Через 2 года после начала работы над GeoArena я, наконец, решил переписать всю свою кодовую базу для использования Webpack. — Это займет вечность, — проворчал я. Если бы я только прочитал сообщение в блоге, объясняющее, почему я должен использовать Webpack…

Вот пример того, что позволяет делать Webpack:

// geoarena-constants.js
const Constants = { version: "1.0.0" };
module.exports = Constants;
// geoarena-menu.js
const Constants = require('./Constants');
console.log('GeoArena Version ' + Constants.version);

Каждый файл представляет собой модуль, который объявляет свои зависимости через require()s и может экспортировать переменные для использования в других модулях. Все, что вам нужно сделать, это запустить

$ webpack

и Webpack сгенерирует пакет, удовлетворяющий зависимостям каждого модуля. Другими словами, Webpack устраняет проблему зависимостей. Больше нет необходимости вручную поддерживать порядок, а зависимости объявляются явно.

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

Резюме

  1. Вначале я просто включал <script> тега для каждого файла Javascript, который у меня был. Это привело к проблеме со скоростью: загрузка большого количества файлов происходит слишком медленно.
  2. Чтобы исправить это, я использовал инструмент сборки для объединения файлов Javascript в один большой пакет, поэтому мне нужен был только один тег <script>. Затем возникла проблема области действия: весь этот код выполнялся в глобальной области действия, что приводило к конфликтам имен.
  3. Я исправил это, завернув каждый файл в IIFE, чтобы сохранить его локальную область. Однако у меня все еще была Проблема зависимостей: зависимости не были объявлены явно, но порядок файлов должен был удовлетворять требованиям зависимостей.

Использование сборщика модулей решает все эти проблемы!

Первоначально опубликовано на сайте victorzhou.com.