webpack: от 0 до автоматизированного тестирования
JavaScript, как и сам язык, и его экосистема, сильно изменился с тех пор, как он впервые появился в 1995 году. Теперь этот язык используется для создания гораздо более крупных инструментов и веб-приложений как в интерфейсе, так и в серверной части, а также для этих больших проектов в JavaScript, люди импортируют много различного стороннего кода. Эти зависимости могут быть такими, как библиотеки с удобной функциональностью JS, такие как Lodash, фреймворки, такие как React, Angular или Vue, код для автоматического тестирования, такой как Mocha или Jest, и даже код, который добавляет к самому языку JavaScript, например Flow для предоставления JS статические типы, которые вы получили бы на таких языках, как C ++ или Go.
Из-за всей этой сложности импорт всего используемого кода JavaScript со стеком из <script>
тегов, например:
<script src="path/to/lodash.js"></script> <script src="path/to/my-helper-functions.js"><script> <script> // This code can now use functions from lodash.js and // my-helper-functions.js </script>
Этим легко управлять, когда вам нужно импортировать всего два скрипта, но как насчет двадцати или сотен? Создание стека из сотен <script>
тегов и их импорт в правильном порядке требует много размышлений и проблем с поддержанием по мере роста и изменения вашего списка зависимостей.
К счастью, управление запутанной сетью отношений между зависимостями - это тот вид мышления, в котором роботы великолепны. Поэтому для создания больших профессиональных веб-приложений автоматизированный процесс сборки является обязательным, и webpack - действительно популярный инструмент для этого. Он строит граф зависимостей для вашего JavaScript, CSS и т. Д., Выводя однофайловые пакеты кода, так что вы можете делать такие вещи, как импорт всего необходимого JavaScript с помощью всего ОДНОГО тега <script>
!
В этом руководстве мы создадим сборку веб-пакета для небольшого веб-приложения, а затем увидим, как использовать веб-пакет для настройки автоматических тестов.
Создание приложения
Для этого примера приложения мы создадим карту, на которой ленивцы смогут найти в Кембридже местные магазины, продающие чай из гибискуса. Потому что каждый ленивец в Cambridge Fresh Pond знает, что чай из гибискуса - лучший чай, чтобы расслабиться!
Чтобы продолжить, создайте каталог с именем типа webpack-mocha-tutorial
, создайте внутри него каталог app/src
и запустите npm init
или yarn init
. Код для приложения находится здесь, а история фиксации репозитория написана для того, чтобы следовать руководству, поэтому я буду ссылаться на каждую фиксацию, чтобы следить за изменениями кода, за исключением фиксации 1, которая просто настраивала репо.
Базовая структура приложения будет выглядеть так:
- У вас есть файл
app/src/distance.js
, который экспортирует функцию, которая запускает формулу расстояния (на самом деле, мы должны использовать формулу расстояния большого круга для географических координат), и функция, которая сообщает нам, какая точка из массива точек ближе всего к вам.
// distance takes in two points, represented as objects with // numeric x and y values and returns the distance between them // // [TODO] Use great-circle distance, not 2D distance, which we're // only using to keep the code in this tutorial short function distance(p2, p1) { let yDist = p2.y - p1.y; let xDist = p2.x - p1.x; return Math.sqrt(Math.pow(yDist, 2) + Math.pow(xDist, 2)); } // sortByDistance takes in your location and an array of points // and returns the array of points sorted function sortByDistance(myPt, points) { return points.sort( (pt1, pt2) => distance(pt1, myPt) - distance(pt2, myPt)); }
- И у вас есть файл с именем
app/src/page.js
, который использует код изdistance.js
, чтобы извлечь ближайший магазин из списка и затем отобразить его на странице.
let stores = [ {name: "Cambridge Naturals", x: -71.1189, y: 42.3895}, {name: "Sarah's Market", x: -71.1311, y: 42.3823}, {name: "Whole Foods Fresh Pond", x: -71.1420, y: 42.3904}, ]; let here = {name: "You are here", x: -71.1470, y: 42.3834}; let nearest = sortByDistance(here, stores)[0]; document.getElementById("nearest-store").innerHTML = nearest.name;
- Наконец, у вас есть веб-страница HTML
index.html
.
<!DOCTYPE html> <html> <head> <title>Closest store with hibiscus tea</title> </head> <body> <p>Nearest store is <span id="nearest-store"></span></p> <script src="app/src/distance.js"></script> <script src="app/src/page.js"></script> </body> </html>
Общая структура каталогов:
Если вы откроете index.html в браузере, то увидите, что в Fresh Pond ближайшим местом, где можно купить чай из гибискуса, является Whole Foods рядом с ротором.
Итак, как вы можете видеть, distance.js
определяет наши функции расстояния, затем page.js
запускается с ними, помещая результат функции sortByDistance в DOM. Но если вы посмотрите на зависимости между вашими файлами, ваш page.js
файл зависит от вашего distance.js
файла, но не наоборот (фиксация 2).
Итак, у вас есть конфигурация, в которой у вас, по сути, есть один файл JavaScript, page.js, в основе графа зависимостей. Так что было бы здорово, если бы вы могли просто index.html импортировать один файл JavaScript с одним тегом <script>
. С помощью webpack вы можете это сделать!
Введите веб-пакет
Как я упоминал в начале, webpack - это инструмент, который позволяет вам взять весь код, который вам нужен для вашего веб-приложения, и преобразовать его в один готовый к работе пакет. Чтобы получить его, используйте npm или yarn для его установки:
yarn add --dev webpack webpack-cli
С помощью этой команды у вас теперь есть webpack и его инструмент командной строки в вашем node_modules
, и вы можете запускать его с помощью инструмента командной строки webpack
. Но прежде чем мы сможем запустить эту сборку веб-пакета, нам нужно, чтобы наш файл page.js действительно импортировал код в distance.js. Итак, у нас есть distance.js экспортировать свои функции, добавив строку:
module.exports = {distance, sortByDistance};
И чтобы page.js использовал экспортированную функцию sortByDistance
, мы добавляем строку:
import {sortByDistance} from "./distance";
Круто, у нас подключены наши зависимости JavaScript. Теперь давайте воспользуемся веб-пакетом для создания нашего приложения! Выполните эту команду:
npx webpack app/src/page.js
И теперь вы должны увидеть новый файл dist/main.js
, который содержит весь ваш код из page.js и distance.js. Теперь попросите index.html
импортировать dist/main.js
вместо ваших скриптов в app/src
, изменив свой HTML следующим образом:
<!DOCTYPE html> <html> <head> <title>Closest store with hibiscus tea</title> </head> <body> <p>Nearest store is <span id="nearest-store"></span></p> <script src="dist/main.js"></script> </body> </html>
Теперь откройте файл в браузере, у вас все еще должен быть работающий код. Этот файл main.js содержит весь код из distance.js и page.js, поэтому вы можете импортировать все из одного файла!
Это работает следующим образом: с помощью команды npx webpack app/src/page.js
вы указываете, что отправной точкой или, в терминологии webpack, точкой входа вашего кода JavaScript является page.js. Итак, webpack читает page.js и видит в этом файле строку import {sortByDistance} from ./distance
. Теперь он знает, что page.js имеет distance.js в качестве зависимости. На основе всех зависимостей в вашем коде webpack строит граф зависимостей и использует его для создания вашего пакета файла JavaScript, dist/main.js
. (Фиксация 3)
Кстати, это также работает с тем, что ваш код также импортирует сторонние зависимости в ваш node_modules
. Давайте попробуем выполнить манипуляции с DOM с помощью jQuery вместо document.getElementById
. Сначала установите jQuery:
yarn add --dev jquery
Затем обновите page.js, чтобы включить и использовать jQuery:
import {sortByDistance} from "./distance"; import $ from "jQuery"; let stores = [ {name: "Cambridge Naturals", x: -71.1189, y: 42.3895}, {name: "Sarah's Market", x: -71.1311, y: 42.3823}, {name: "Whole Foods Fresh Pond", x: -71.1420, y: 42.3904}, ]; let here = {name: "You are here", x: -71.1470, y: 42.3834}; let nearest = sortByDistance(here, stores)[0]; $("#nearest-store").html(nearest.name);
Теперь ваш график зависимостей:
И если вы выполните npx webpack app/src/page.js
и перезагрузите index.html, хотя размер вашего файла dist / main.js намного больше из-за того, что он содержит код из jQuery, ваше веб-приложение все равно будет работать!
Прежде чем продолжить, зайдите в свой файл package.json и добавьте эти три строки:
"scripts": { "build": "webpack app/src/page.js" }
Теперь, если вы запускаете сборку веб-пакета, вы можете сделать это, просто набрав yarn build
вместо того, чтобы запоминать npx webpack app/src/page.js
. Помимо упрощения ввода, если ваша команда сборки изменится, вы можете просто обновить эту строку файла package.json с помощью новой команды сборки, а затем вы по-прежнему можете создавать приложение с помощью yarn build
вместо того, чтобы вы и ваша команда разработчиков привыкли к запуск новой команды. (Фиксация 4)
Настройка webpack с помощью файла webpack.config.js
То, что вы видели с этой командой npx webpack app/src/page.js
, было поведением webpack по умолчанию. Если вы запустите webpack [entry-file.js]
, тогда webpack построит граф зависимостей из этого файла записи и выведет файл пакета в dist/main.js
. Но вы можете контролировать, где находятся ваши точки входа и выхода, если вы настроите webpack с помощью файла конфигурации. Поместите этот код в файл в каталоге webpack-mocha-tutorial с именем webpack.config.js
:
module.exports = { entry: __dirname + "/app/src/page.js", output: { path: __dirname + "/dist/", } }
Теперь, если вы запустите npx webpack
или можете выполнить ту же сборку, что и раньше, без указания точки входа в аргументах командной строки, потому что теперь она находится в webpack.config.js
! Это также означает, что вы можете обновить скрипт сборки вашего package.json
файла, чтобы он просто:
"build": "webpack",
Если бы вы изменили выходной путь в конфигурационном файле webpack на что-то вроде __dirname + "/somewhere_else"
, то повторный запуск команды yarn build
поместил бы связанный файл в somewhere_else/main.js
. (Фиксация 5)
Но файл конфигурации веб-пакета предназначен не только для настройки расположения ваших файлов ввода и вывода. Вы также можете настроить, что webpack делает при обнаружении файлов различного типа в графе зависимостей, используя загрузчики, которые в основном представляют собой программы JavaScript, которые тем или иным образом преобразуют ваш код. Например, у вас может быть правило в файле конфигурации веб-пакета, согласно которому, если веб-пакет встречает файл TypeScript в своем графе зависимостей, этот файл отправляется через загрузчик, который преобразует его из TypeScript в обычный JavaScript.
Загрузчик, который мы собираемся использовать, - это загрузчик Babel. Если вы не использовали его раньше, Babel - это инструмент, который берет JS-код, использующий современные функции, и преобразует его в обратно совместимый эквивалент. Это позволяет вашему приложению работать в старых браузерах или, в более общем плане, в браузерах, которые еще не поддерживают некоторые новые функции JavaScript. В конце концов, некоторые ленивцы, использующие наше приложение, не обновляли свои браузеры с 2009 года. И одна часть кода, который мы написали, не будет работать в браузере 2009 года:
return points.sort((pt1, pt2) => distance(pt1, myPt) — distance(pt2, myPt));
Мы используем стрелочную функцию, а ее еще не изобрели. Итак, давайте воспользуемся babel-loader, чтобы отправить эту стрелочную функцию в прошлое! Первый забег:
yarn add --dev babel-core [email protected] babel-preset-env
Затем в вашем webpack.config.js добавьте этот код к объекту module.exports
:
module: { rules: [ { test: /\.js$/, exclude: ["/node_modules/"], use: [ { loader: "babel-loader", options: { presets: ["env"], }, }, ], }, ], },
Это добавляет новое правило к нашим сборкам веб-пакетов. Если webpack обнаруживает файл в дереве зависимостей, который заканчивается на .js
(например, distance.js), и этого файла нет в node_modules
(например, jQuery), то наше правило применяется к этому файлу.
Любой файл, соответствующий этому правилу, затем проходит через все загрузчики в массиве use
правила (который в нашем случае является только загрузчиком babel). Итак, distance.js
и page.js
запускаются через babel-loader, что приводит к удалению стрелочной функции distance.js
, а затем webpack продолжает свой веселый путь построения вашего пакета. Между тем, когда webpack встречает jQuery, он просто загружает этот код без загрузчика, поскольку jQuery находится в каталоге node_modules
.
Если вы запустите yarn build
и войдете в исходный код для dist/main.js
, код, соответствующий вашей функции сортировки, теперь будет использовать ключевое слово function
, а не стрелочную функцию! (Фиксация 6)
Итак, наше приложение готово к концу нулевых! Но чтобы поддерживать этот код, мы должны написать для него несколько тестов.
Добавление тестового покрытия в нашу сборку
Давайте добавим тестовое покрытие в наш файл distance.js. Мы будем использовать Mocha, инструмент на основе набора тестов для написания тестов, и Chai в качестве нашей библиотеки утверждений, поэтому выполните эту команду:
yarn add --dev mocha chai
Затем создайте новый каталог app/test
и новый файл app/test/distance.test.js
, содержащий этот код:
import {expect} from "chai"; import {distance, sortByDistance} from "../src/distance"; describe("distance", function() { it("calculates distance with the good ol' Pythagorean Theorem", function() { let origin = {x: 0.0, y: 0.0}; let point = {x: 3.0, y: 4.0}; expect(distance(point, origin)).to.equal(5); }); }); describe("sortByDistance", function() { it("sortsByDistance", function() { let places = [ {name: "Far away", x: 100, y: 50}, {name: "Nearby", x: 20, y: 10}, ]; let origin = {name: "Origin", x: 0, y: 0}; let sorted = sortByDistance(origin, places); expect(sorted[0].name).to.equal("Nearby"); expect(sorted[1].name).to.equal("Far away"); }); });
У нас есть наши тестовые функции для наших distance
и sortByDistance
функций, утверждающих, что функция расстояния вычисляет формулу расстояния, а функция sortByDistance сортирует массивы координат, используя формулу расстояния, используя наборы тестов Mocha и утверждения Chai. Довольно стандартная тестовая установка.
Однако, если мы запустим mocha app/test/distance.test.js
, мы получим сообщение об ошибке, что наш JavaScript недействителен, поскольку он содержит ключевое слово import
, которое в настоящее время Node не поддерживает. Но что, если мы обойдем это ограничение, используя webpack для управления зависимостями нашего тестового кода? (Примечание: это также можно тривиально исправить, просто используя require
вместо import
в наших тестовых файлах, но у вас также будет процесс сборки для тестового кода, если вы тестируете такие вещи, как Flow-typed JavaScript, который использует аннотации типов, или Веб-приложения Vue.js, которые используют файлы .vue, поскольку оба из них необходимо перенести в обычный JS).
Наш процесс тестирования для этого будет:
- Пусть webpack строит деревья зависимостей, начиная с наших тестовых файлов, а не с одного из файлов нашего приложения.
- Затем webpack создаст файл JavaScript, содержащий весь наш тестовый код и его зависимости без ключевого слова
import
- Затем мы проводим наши тесты, запустив Mocha в этом файле JavaScript.
что выглядит так:
Как видите, это также означает, что мы будем делать две отдельные сборки. Один из них имеет код нашего приложения в качестве точки входа и папку dist
в качестве выходного каталога, а другой - наши тестовые файлы в качестве точки входа и test-dist
в качестве выходного каталога. Итак, давайте обновим наш конфигурационный файл webpack для поддержки этой второй сборки:
let glob = require("glob"); let entry = __dirname + "/app/src/page.js"; let outputPath = __dirname + "/dist/"; if (process.env.TESTBUILD) { entry = glob.sync(__dirname + "/app/test/**/*.test.js"); outputPath = __dirname + "/test-dist/"; } module.exports = { entry: entry, output: { path: outputPath, }, // rest of webpack config file stays the same
Что это значит? В пятой строке у нас есть оператор if, который мы запускаем, если у нас есть непустое значение для нашей переменной среды TESTBUILD. Итак, если бы мы запустили TESTBUILD=true webpack
, мы бы ввели этот оператор if, но мы бы этого не сделали, если бы просто запустили npx webpack
.
Внутри этого оператора if мы меняем JS-файл, который является нашей точкой входа. Вместо того, чтобы наш выходной путь шел в папку dist
, он попадает в папку test-dist
. И вместо app/src/path.js
в качестве нашей точки входа нашей точкой входа теперь является массив всех файлов, соответствующих выражению glob app/test/**/*.test.js
. Другими словами, это все файлы, которые:
- в пути в каталоге
app/test
и - есть путь, заканчивающийся на
.test.js
Мы передаем нашу новую точку входа и путь вывода в объект module.exports
, и webpack запускается с этим, чтобы создать нашу тестовую сборку. Как видите, конфигурация webpack представляет собой обычный JavaScript, поэтому мы можем использовать стандартную библиотеку Node и операторы if для ее настройки, как JavaScript. Запустите TESTBUILD=true npx webpack
, и вы должны увидеть каталог test-dist
. И если вы запустите npx mocha test-dist/main.js
, вы должны увидеть, как выполняются ваши тесты!
Наконец, в разделе «сценарии» вашего package.json
добавьте эту строку:
"test": "TESTBUILD=true webpack && mocha test-dist/main.js && rm -rf test-dist"
Это означает, что теперь, когда вы запускаете yarn test
, вы делаете свою test-dist
сборку с помощью webpack, затем запускаете Mocha в этой сборке и, наконец, rm -rf test-dist
удаляет каталог test-dist, поскольку мы его использовали. (Фиксация 7)
Сопоставление исходного кода нашего тестового кода
Теперь у нас есть тестовая сборка, но есть одна вещь, которая может раздражать, когда мы тестируем наш код. Если мы запускаем Mocha для нашего test-dist/main.js
file и один из наших тестов не проходит, как это будет выглядеть? Давайте сделаем так, чтобы наша проверка формулы расстояния провалилась в app/test/distance.test.js
:
describe("distance", function() { it("calculates distance with the good ol' Pythagorean Theorem", function() { let origin = {x: 0.0, y: 0.0}; let point = {x: 3.0, y: 4.0}; expect(distance(point, origin)).to.equal(2071); }); });
Запустите yarn test
, и вы должны получить это
Тест не пройден, но вы не можете увидеть, какая это была строка исходного тестового кода, а если у вас много тестов для вашего веб-приложения, эту строку с ошибкой будет сложно найти.
Неисправный код находится в строке 8 app/test/distance.test.js
, но мы запускаем Mocha на test-dist/main.js
, поэтому с точки зрения Mocha ошибочное утверждение находится в строке 116. К счастью, webpack поддерживает исходные карты, которые могут вам сказать какая строка кода соответствует ошибке. Исходная карта подобна кольцу декодера в специально помеченной коробке с хлопьями, и вы берете свое кольцо декодера и связанный файл main.js, чтобы вернуть исходные строки кода, соответствующие объединенному коду. Часть этого полного завтрака, а теперь часть этой полной конфигурации веб-пакета! Обновите оператор if в вашем webpack.config.js
файле:
let entry = __dirname + "/app/src/path.js"; let outputPath = __dirname + "/dist/"; let devtool = ""; if (process.env.TESTBUILD) { entry = glob.sync(__dirname + "/app/test/**/*.test.js"); outputPath = __dirname + "/test-dist/"; devtool = "source-map"; }
Затем в объекте module.exports добавьте строку:
devtool: devtool,
Теперь в ваших тестовых сборках ваш каталог test-dist будет содержать файл исходной карты. Запустите npx webpack TESTBUILD=true
, и в вашем каталоге test-dist
будет файл main.js.map
, а также пакет main.js
.
Чтобы Mocha мог использовать эту исходную карту при запуске тестов, нам нужно установить еще один пакет:
yarn add --dev source-map-support
Теперь, чтобы использовать его, нам просто нужно обновить скрипт Mocha в разделе scripts.test
нашего package.json
:
TESTBUILD=true webpack && mocha test-dist/main.js --require source-map-support/register && rm -rf test-dist
Этот флаг в Mocha, --require source-map-support/register
указывает, что Mocha требует пакета поддержки исходной карты, что означает, что Mocha будет использовать исходную карту, если она доступна. Итак, теперь, если вы запустите yarn test
, когда вы получите неудавшееся утверждение, вы будете знать, в какой строке оно находится, и сможете исправить код!
Лола соглашается, что исходные карты действительно там, где они есть! (Фиксация 8)
Итак, теперь у вас есть настройка как для обычных сборок дистрибутива, так и для тестовой сборки с сопоставлением источников. С этого момента существует множество других способов, которые вы можете использовать на этом, например, объединение нескольких загрузчиков JavaScript вместе для обработки вашего кода в конвейере или запуск веб-пакета в качестве сервера разработки, чтобы мгновенно увидеть влияние изменений вашего кода на окончательную сборку веб-пакета. , так что продолжайте пробовать наши различные пакеты, чтобы составить webpack.config.js
файл для вашего приложения, потому что мы только прикоснулись к нему.
До следующего раза, ОСТАВАЙТЕСЬ ПРОСТО!
Фотография Sloth была сделана Jenny Jozwiak и находится под лицензией CC BY 2.0.