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).

Наш процесс тестирования для этого будет:

  1. Пусть webpack строит деревья зависимостей, начиная с наших тестовых файлов, а не с одного из файлов нашего приложения.
  2. Затем webpack создаст файл JavaScript, содержащий весь наш тестовый код и его зависимости без ключевого слова import
  3. Затем мы проводим наши тесты, запустив 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. Другими словами, это все файлы, которые:

  1. в пути в каталоге app/test и
  2. есть путь, заканчивающийся на .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.