Почему Вебпак? (или Как не обслуживать 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 сценария. 😬😬
У этого подхода есть несколько больших проблем:
- Скорость. Запрос такого большого количества скриптов был узким местом в сети, и в результате мой сайт загружался несколько медленно (имейте в виду, что это было в 2016 году, до эры HTTP/2). . Веб-производительность имеет значение — его важность давно известна и задокументирована. Что вам кажется более эффективным: запрашивать 10 строк кода 100 раз или запрашивать 1000 строк кода один раз?
- Область действия. Каждый файл работал в одной и той же глобальной области видимости, поэтому любая объявленная мной переменная была доступна в глобальном объекте окна. Это означало, что все, что я объявлял, должно было иметь уникальное имя — иначе возникла бы коллизия! Вы видите, насколько это проблематично? Больше кода = больше переменных = больше Подождите, я уже использовал это имя раньше?
- Зависимости. Мне пришлось вручную поддерживать порядок включения
<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, чтобы узнать больше.
Резюме
- Вначале я просто включал
<script>
тега для каждого файла Javascript, который у меня был. Это привело к проблеме со скоростью: загрузка большого количества файлов происходит слишком медленно. - Чтобы исправить это, я использовал инструмент сборки для объединения файлов Javascript в один большой пакет, поэтому мне нужен был только один тег
<script>
. Затем возникла проблема области действия: весь этот код выполнялся в глобальной области действия, что приводило к конфликтам имен. - Я исправил это, завернув каждый файл в IIFE, чтобы сохранить его локальную область. Однако у меня все еще была Проблема зависимостей: зависимости не были объявлены явно, но порядок файлов должен был удовлетворять требованиям зависимостей.
Использование сборщика модулей решает все эти проблемы!
Первоначально опубликовано на сайте victorzhou.com.