diff2html: сравнение кода и настроек — мастерская и 8 примеров
Сравниваем версии кода, конфигураций и документов: восемь редактируемых примеров, две колонки, unified diff, JSON и автономный HTML-отчёт. Исходники для Windows и Linux.

Сравниваем код, конфигурации и документы: восемь практических примеров, редактируемая мастерская, patch, JSON и автономный HTML-отчёт. Исходники и команды для Windows и Linux.
Что такое diff2html и какой результат вы получите
Когда меняется обработчик заказа, конфигурация сервера или инструкция для сотрудников, важно увидеть конкретную разницу между версиями. Два длинных файла рядом заставляют вручную искать изменённые строки. Отчёт сравнения собирает изменения в одном месте: показывает добавления, удаления, номера строк и небольшой фрагмент окружения. Такой отчёт можно встроить в административную панель, приложить к релизу или передать коллеге для проверки.
diff2html превращает текстовый diff в HTML-представление. В этой статье мы соединим его с jsdiff, который вычисляет изменения между двумя строками, и получим готовую мастерскую. На странице можно сравнивать PHP, JavaScript, HTML, CSS, JSON, Compose-конфигурацию и документацию. Для выпуска из нескольких файлов предусмотрен отдельный пример со списком изменений.
Вы сможете отредактировать обе версии, переключить вид, настроить количество строк контекста и скачать три результата: patch, JSON со структурой сравнения и автономный HTML-отчёт. В исходниках есть браузерная мастерская, консольный генератор и тесты. Все примеры используют закреплённые версии diff2html 3.4.56 и jsdiff 9.0.0; установка и запуск разобраны для Windows и Linux.
Где применять: ревью, административные панели и согласование
Первое применение — просмотр изменений перед выпуском. Разработчик выбирает две версии, формирует patch и отдаёт его на проверку. Рецензент начинает со списка файлов, затем рассматривает отдельные строки и окружающий код. Это помогает обсуждать конкретное изменение вместо сообщения «я немного поправил обработчик». Само сравнение не проверяет правильность алгоритма: оно показывает материал для осмысленного ревью.
В административной панели полезен другой сценарий: редактор изменил шаблон письма, параметры интеграции или текст справочной статьи. Перед сохранением интерфейс показывает «было» и «стало». Для такого инструмента нужны история версий, права доступа и подтверждение сохранения. diff2html отвечает только за визуализацию различий; хранение, авторизация и запись новой версии остаются задачами вашего приложения.
Третий вариант — отчёт для поддержки или заказчика. HTML-файл открывается отдельно от проекта, поэтому получателю не нужно устанавливать Node.js. В нём остаются имена файлов и изменённые строки. Заранее выберите объём: для обсуждения настройки таймаута обычно достаточно одного файла, а огромная выгрузка всего репозитория только затрудняет проверку.
jsdiff, unified diff и diff2html: три части решения
У нашей цепочки три отдельных шага. Сначала jsdiff сравнивает старую и новую строки. Затем результат записывается в unified diff: текст с заголовками файлов и фрагментами изменений. Наконец diff2html разбирает этот текст и создаёт HTML. Такое разделение позволяет менять источник: готовый patch можно получить не только из JavaScript, но и командой git diff.
Это удобно при интеграции с существующим сервером. Если backend уже отдаёт unified diff, браузеру не нужно получать целиком обе версии и заново искать различия. Если у вас редактор двух коротких текстов, сравнение можно выполнить на клиенте. В обоих случаях формат представления одинаковый, но стоимость передачи данных и место выполнения вычислений отличаются.
Наша мастерская поддерживает обе формы ввода. В режиме двух текстов доступны имя файла и размер контекста. В режиме готового patch эти сведения уже находятся в заголовках, поэтому соответствующие настройки отключены. Выбор представления — последовательные строки или две колонки — применяется к обоим режимам.
import { createTwoFilesPatch } from 'diff';
import { html } from 'diff2html';
const patch = createTwoFilesPatch(
'a/example.txt', 'b/example.txt',
'Старая строка\n', 'Новая строка\n', '', '', { context: 3 }
);
const markup = html(patch, { outputFormat: 'line-by-line' });
Мини-мастерская: попробуйте восемь задач
Начните с готового примера и прочитайте подсказку рядом с выбором сценария. Измените несколько строк в поле «Стало», затем нажмите «Сравнить версии». Пока результат не пересчитан, скачивание отключено: так старый отчёт не выдаётся за новую редакцию текста. Кнопка возврата примера восстанавливает исходные учебные данные выбранной сцены.
Переключите представление на две колонки, затем вернитесь к последовательным строкам. На узком экране обычно проще читать одну колонку; широкое сравнение остаётся внутри собственной области прокрутки. Для больших фрагментов используйте отдельное окно мастерской. Работа происходит локально в браузере: введённые тексты не отправляются обработчику на нашем сервере.
Скачайте patch и HTML-отчёт, чтобы увидеть разницу форматов. Первый предназначен для инструментов разработки, второй — для просмотра человеком. JSON полезен, когда собственный интерфейс хочет посчитать изменённые файлы или построить список разделов. Экспорт сохраняет текущий успешно рассчитанный результат, а не просто содержимое полей ввода.
Как читать заголовки файлов и фрагменты @@
В unified diff строка с тремя минусами обозначает старый файл, а строка с тремя плюсами — новый. Префиксы a/ и b/ помогают различать стороны; в отчёте обычно остаётся понятное имя самого файла. Ниже идут фрагменты изменений, которые называют hunks. Каждый начинается с заголовка @@, содержащего координаты старого и нового диапазонов.
Например, -2,3 означает три строки старой версии, начиная со второй, а +2,4 — четыре строки новой версии. Внутри фрагмента пробел в первом столбце означает неизменённый контекст, минус — удаление, плюс — добавление. Замена обычно представлена удалением прежней строки и добавлением новой. Поэтому счётчик добавлений не равен количеству новых функций или исправленных ошибок.
Вручную редактируя patch, легко нарушить длины диапазонов. Мастерская проверяет его парсером jsdiff перед отображением, чтобы некорректный счётчик не выглядел готовым отчётом. Но корректная структура patch ещё не означает, что его можно применить к любой версии файла: для применения нужен соответствующий исходный текст и отдельная проверка.
--- a/settings.txt
+++ b/settings.txt
@@ -1,2 +1,2 @@
timeout=1000
-retries=0
+retries=2
PHP: просматриваем изменение обработчика
В первом примере простое приведение параметра page к целому заменено явной проверкой через FILTER_VALIDATE_INT. Отчёт показывает добавленный блок условия и сохранённый вызов каталога. Это хороший повод обсудить договорённость: неверная страница превращается в первую или должна возвращать ошибку? Визуальное сравнение помогает найти участок, где реализована выбранная политика.
Попробуйте добавить ограничение максимальной страницы и сравните снова. Вы увидите, какие строки потребуют дополнительного теста. Одинаковый цвет добавления не делает код безопасным или правильным автоматически: рецензент должен проверить типы, поведение null и false, границы и взаимодействие с реальной функцией загрузки.
Этот фрагмент служит материалом для сравнения, а не автономным PHP-приложением: loadCatalog и respond здесь обозначают функции вашего проекта. Сама мастерская не запускает PHP. Она отображает его исходный текст. Для выполнения серверного обработчика понадобится PHP-окружение, набор данных и отдельные тесты.
<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
if ($page === false || $page === null || $page < 1) {
$page = 1;
}
$items = loadCatalog($page);
respond($items);
JavaScript: изменение функции пагинации
Во второй сцене размер страницы становится параметром функции, меняется его значение по умолчанию и появляется проверка индекса. Просматривая patch, полезно разделить изменение интерфейса функции и изменение её поведения. Вызывающий код, который передавал только два аргумента, продолжит работать, но получит другой размер страницы.
Измените текст ошибки или значение size и посмотрите выделение внутри строки. Оно помогает быстро увидеть небольшую замену среди похожих строк. При этом совпадение строк строится алгоритмом сравнения текста, а не анализатором JavaScript: перенос функции или изменение форматирования может выглядеть как большой блок удаления и добавления.
В реальном ревью попросите рядом тесты для пустого списка, отрицательного индекса и последней страницы. Мастерская показывает, что поменялось, а проверка поведения подтверждает, что новые ограничения работают. Такая связка особенно полезна перед заменой утилиты, которая используется сразу в нескольких экранах административной панели.
export function page(items, index, size = 20) {
if (!Number.isInteger(index) || index < 0) {
throw new RangeError('Invalid page index');
}
return items.slice(index * size, (index + 1) * size);
}
HTML: обсуждаем доступность формы
В HTML-примере у поля появляется явная подпись, тип search и имя параметра. У кнопки задан type, добавлена область сообщений с aria-live. Это практический случай для согласования интерфейса: разработчик и специалист по удобству могут обсуждать конкретные строки, а затем проверить результат на настоящей странице.
Исходный HTML внутри сравнения остаётся текстом. Теги input или script из ваших полей не должны превращаться в работающие элементы страницы. Дополнительно сгенерированная разметка проходит DOMPurify. Это позволяет использовать мастерскую для показа примеров HTML без исполнения самого сравниваемого документа.
Однако наличие label и aria-live в diff не доказывает доступность формы целиком. Нужно проверить связь подписи с полем, порядок фокуса, сообщения об ошибках и управление с клавиатуры. При использовании отчёта как части ревью полезно прикладывать ссылку на проверяемый интерфейс, чтобы текстовые изменения можно было соотнести с поведением.
<form>
<label for="q">Найти материал</label>
<input id="q" type="search" name="q" autocomplete="off">
<button type="submit">Найти</button>
<p role="status" aria-live="polite"></p>
</form>
CSS: показываем мобильные изменения
Сцена CSS меняет ширину колонок, расстояние между карточками и добавляет медиазапрос для узкого экрана. В diff хорошо видно, что правило minmax(0, 1fr) относится к существующей сетке, а новая секция отвечает за мобильный вариант. Так можно объяснить причину правки без пересылки целого файла стилей.
Поменяйте breakpoint и число колонок. Затем уменьшите контекст до одной строки: отчёт станет короче, но часть соседних правил исчезнет. Для простой настройки этого достаточно. Если изменение зависит от каскада, специфичности или другого медиазапроса, рецензенту нужен больший контекст либо весь исходный файл.
Сравнение CSS не заменяет адаптивную проверку. Браузер может по-разному разложить карточки при длинном заголовке или нестандартном размере шрифта. Поэтому в рабочем процессе сначала рассматривают diff, затем открывают страницу на нужных ширинах и проверяют реальный контент. В архиве мастерской такой порядок применён к её собственной вёрстке.
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
@media (max-width: 640px) {
.cards { grid-template-columns: 1fr; }
}
JSON: сравниваем параметры сервиса
В конфигурации изменяются флаг поиска, размер страницы и таймаут, добавляется количество повторов. Для администратора это читаемый список настройки релиза: видно прежнее значение и предлагаемое. Но инструмент сравнивает текст, поэтому перестановка ключей или другая вложенность отступов тоже создаёт различия.
Если вам нужно сравнивать смысл JSON, сначала определите правила нормализации. Можно распарсить документ, стабильно отсортировать ключи объектов и сериализовать одинаковым способом. Порядок массивов при этом часто значим: сортировка списка маршрутов или приоритетов способна скрыть реальное изменение поведения. Универсальная кнопка «убрать все различия порядка» здесь опасно упрощает задачу.
В нашей сцене нормализация не выполняется автоматически. Это сохраняет точную связь с введёнными файлами. Отдельный семантический режим можно добавить в собственный продукт, но он должен явно называться и объяснять, какие преобразования выполнены перед сравнением. Исходные версии стоит сохранять независимо от представления отчёта.
{
"feature": {
"search": true
},
"pageSize": 20,
"timeoutMs": 2500,
"retry": 2
}
Compose: рассматриваем настройку healthcheck
В примере Compose добавлен healthcheck со временем ожидания, периодом и количеством попыток. Код команды проверяет готовность приложения с помощью отдельного healthcheck.mjs. Сам этот файл в сцене не исполняется: мы рассматриваем изменение конфигурации как объект ревью.
Уменьшите контекст, чтобы оставить только добавленный блок, затем увеличьте его и посмотрите настройки сервиса целиком. Во втором варианте легче заметить рабочий каталог и способ запуска. Для эксплуатационного изменения полезно обсуждать не только синтаксис YAML, но и то, существует ли проверяемый маршрут, что означает статус ready и какие действия предпринимает система после ошибки.
Подробное применение контейнеров разобрано в материале о Docker Compose. Здесь задача уже: показать человеку отличие конфигурации перед её применением. Кнопка экспорта отчёта не запускает Docker, не меняет контейнеры и не применяет настройки сервера.
services:
api:
image: node:24-bookworm-slim
working_dir: /app
command: node server.mjs
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test: ["CMD", "node", "healthcheck.mjs"]
interval: 30s
timeout: 5s
retries: 3
Документация: согласуем новую инструкцию
Документация меняется не реже программного кода. В седьмой сцене уточняются версия Node.js, команда установки зависимостей, тесты и способ доступа к сервису. Визуальное сравнение помогает техническому писателю и инженеру поддержки быстро согласовать именно новые требования, не перечитывая инструкцию целиком.
Попробуйте заменить несколько русских слов и скачать автономный отчёт. В нём сохранятся текст и выделение различий. Получатель сможет открыть файл без установленной библиотеки. При этом Markdown в исходнике остаётся Markdown-текстом: заголовки и ссылки не превращаются в отдельную отрендеренную статью.
Для сравнения готовых документов может понадобиться дополнительный этап извлечения текста из Word или PDF. Не выдавайте такое извлечение за точное сравнение визуального оформления: колонтитулы, переносы и таблицы часто требуют отдельной обработки. diff2html лучше всего подходит там, где обе версии уже представлены сопоставимым текстом.
# Запуск проекта
Установите Node.js 24 LTS и зависимости через npm ci.
Запустите npm test, затем npm start.
Сервис доступен только на 127.0.0.1:3000.
Для удалённого сервера используйте SSH-туннель.
Несколько файлов: обзор небольшого релиза
Последний пример объединяет изменение кода, теста и README в один patch. В начале результата появляется список файлов, а каждый раздел имеет свой заголовок и статистику. Такой обзор удобен для небольшого выпуска: рецензент видит, что новая логика сопровождается изменением проверки и документации.
Выберите нужный файл в списке. Затем переключите режим отображения и сравните длину отчёта. На мобильном широкие колонки прокручиваются внутри области результата, а основные кнопки остаются в обычном потоке страницы. Для длинного обзора полезно открыть мастерскую отдельно, чтобы увеличить доступную область просмотра.
Входом служит обычный unified diff с несколькими секциями. Это не формат combined diff для merge-коммита и не вывод word-diff. Если исходный инструмент сформировал другой формат, подготовьте обычный patch заново. Попытка скрыть несовместимость красивой HTML-обёрткой только затруднит чтение неправильного отчёта.
const patch = files.map(file =>
createTwoFilesPatch('a/' + file.name, 'b/' + file.name,
file.before, file.after, '', '', { context: 3 })
).join('\n');
Контекст, две колонки и различия внутри строки
Контекст — неизменённые строки вокруг правки. Он помогает понять положение изменения и связь с соседним кодом. Ноль строк делает patch компактным, но часто лишает читателя важного окружения. Три строки подходят для многих небольших правок; шесть или двенадцать полезны, когда нужно увидеть условие или соседний вызов.
Режим line-by-line помещает удалённые и добавленные строки друг под другом. Он лучше использует узкую ширину и удобен для длинных текстовых строк. Side-by-side разделяет старую и новую версии на две колонки, облегчая сопоставление соседних изменений на широком экране. Выбор зависит от материала и устройства, а не от того, какой режим выглядит эффектнее.
Выделение слов внутри строки помогает заметить точечную замену. Но оно не понимает значения переменных, выражений или конфигурационных параметров. Например, изменение одного знака в условии может быть важнее десятков новых строк. Счётчики и подсветка помогают ориентироваться; оценка последствий остаётся отдельной задачей.
const markup = html(patch, {
outputFormat: 'side-by-side',
drawFileList: true,
matching: 'lines',
diffStyle: 'word'
});
Скачивание patch, JSON и автономного HTML
Patch сохраняет текст различий. Его можно передать другому инструменту, приложить к задаче или использовать для последующей проверки применения. Само скачивание не меняет файлы. В нашей мастерской применения patch вообще нет: это намеренно отдельная операция, для которой нужно выбрать целевой проект и проверить исходную версию.
JSON содержит разобранные файлы, блоки, строки и общие счётчики. Он подходит для построения собственного интерфейса: например, списка изменённых компонентов. Это структура парсера закреплённой версии библиотеки, а не универсальный стандарт обмена. Если сохраняете её надолго, добавьте версию формата своего приложения и проверяйте совместимость при обновлении зависимости.
HTML-отчёт включает стили и очищенную разметку. Он не загружает JavaScript и открывается без npm. Политика CSP запрещает выполнение скриптов; сравниваемый код остаётся текстом. Перед передачей отчёта помните, что удалённые строки тоже попадают в файл. Если из настройки убрали секрет, он может остаться именно в красной части сравнения.
Установка проекта: что находится в исходниках
Архив содержит package.json с точными версиями, package-lock.json, код интерфейса, Web Worker, консольный генератор, восемь наборов данных и тесты. Папка public уже собрана, поэтому её можно обслуживать обычным статическим сервером. Для изменения и повторной сборки понадобится Node.js 24.15 или новее в поддерживаемой ветке 24.
Зависимости восстанавливаются командой npm ci. В учебном проекте установка выполняется с –ignore-scripts, после чего сборка запускается явно. Это делает шаги понятными: сначала пакеты из lock-файла, затем тесты и создание браузерных файлов. Не редактируйте минифицированный файл в public/assets как основной исходник — изменения нужно вносить в src.
jsdom нужен консольному генератору для DOMPurify в Node.js, поэтому он включён в зависимости проекта. Браузерная сборка его не загружает. Аналогично сервер для локального просмотра и тестовые команды не превращаются в публичные API. Веб-публикации достаточно содержимого public; файл с исходным проектом предназначен для отдельного скачивания.
diff2html-project/
src/ # browser UI, worker, parser wrapper, CLI
fixtures/ # two small UTF-8 texts for CLI
tests/ # patch round-trip, limits, HTML and CLI checks
tools/ # build and loopback static server
public/ # prepared workshop
package.json
package-lock.json
Windows: запуск из PowerShell
Распакуйте архив в обычный рабочий каталог и откройте в нём PowerShell. Проверьте node –version и npm –version. Использование npm.cmd в командах ниже позволяет не зависеть от политики выполнения PowerShell-обёртки npm.ps1. Оно не меняет политику безопасности компьютера и не требует запуска терминала от администратора.
После установки выполните тесты и сборку, затем запустите локальный сервер. Откройте адрес 127.0.0.1:8185 в браузере. Сервер слушает только loopback, поэтому предназначен для работы на этом компьютере. Не открывайте index.html двойным щелчком: модульные скрипты, Worker и загрузка CSS должны обслуживаться через HTTP.
Для завершения нажмите Ctrl+C в том терминале, где запущен serve. Команда сборки не устанавливает службу и не добавляет задачу в планировщик. Если порт занят, выберите другой через переменную PORT для текущего процесса, а после работы удалите её. Чужой процесс на занятом порту останавливать ради примера не требуется.
Expand-Archive -LiteralPath .\diff2html-project.zip -DestinationPath .\demo
Set-Location .\demo\diff2html-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd test
npm.cmd run build
npm.cmd run serve
# Open http://127.0.0.1:8185/
# Optional different port:
# $env:PORT = '8186'
# npm.cmd run serve
# Remove-Item Env:PORT
Linux: локальная машина и удалённый сервер
На Linux команды проекта те же: установить зависимости, проверить тесты, собрать public и запустить просмотр. Node.js устанавливается отдельно из подходящего доверенного источника для вашего дистрибутива. Не заменяйте версию работающего приложения на сервере только ради лаборатории; сначала проверьте проект в отдельном каталоге и совместимом окружении.
Если работа идёт по SSH, адрес 127.0.0.1 в вашем браузере относится к вашему компьютеру, а не к VPS. Откройте SSH-туннель с локального компьютера на удалённый loopback-порт. Тогда мастерская будет доступна через локальный адрес без публичного открытия учебного Node-сервера. Туннель существует, пока работает соответствующий SSH-процесс.
Для постоянной публикации используйте собранную статику и существующий HTTPS-сервер. Node.js нужен для сборки и консольного генератора, но не обязателен для обслуживания готового браузерного интерфейса. Такой вариант проще сопровождать: на сервер попадают только необходимые публичные файлы, а изменения исходников проходят отдельный цикл сборки.
unzip diff2html-project.zip -d demo
cd demo/diff2html-project
node --version
npm ci --ignore-scripts
npm test
npm run build
npm run serve
# Run this on your local computer, not inside the server SSH session:
ssh -L 8185:127.0.0.1:8185 user@example.com
CLI: создаём отчёт из двух UTF-8 файлов
Консольный генератор принимает три аргумента: старый файл, новый файл и путь HTML-отчёта. Он читает тексты, строит patch, формирует представление, очищает HTML и сохраняет автономный документ. Имена файлов с пробелами нужно заключать в кавычки в своей оболочке. Содержимое исходных файлов не изменяется.
Генератор использует флаг записи wx: если целевой отчёт уже существует, команда завершится ошибкой и сохранит прежний файл. Это полезно для повторяемой лаборатории и предотвращает случайную перезапись результата. Для нового запуска выберите другое имя отчёта либо отдельно решите, что делать со старым документом.
Вход ограничен размером 128 КиБ на файл и числом строк. Большой репозиторий удобнее обрабатывать средствами Git, выбирая нужные пути и коммиты, а не превращать всю историю в один HTML. Для автоматизации проверяйте код завершения и наличие результата; сообщение в терминале содержит число файлов, добавленных и удалённых строк.
node src/cli.mjs fixtures/before.txt fixtures/after.txt report.html
# Another name for the next run:
node src/cli.mjs fixtures/before.txt fixtures/after.txt report-2.html
Git: получаем patch для своей задачи
Если проект находится в Git, источник изменений уже существует. Команда git diff показывает рабочие изменения относительно индекса, –cached — подготовленные изменения, а сравнение двух коммитов позволяет рассмотреть конкретный выпуск. Выберите область осознанно: отчёт разных команд отвечает на разные вопросы, даже если выглядит одинаково.
Параметр –output записывает patch напрямую, что удобно на Windows и Linux и избегает различий кодировок перенаправления старых оболочек. Для чтения в мастерской нужен обычный unified diff. Не используйте –word-diff и combined diff в качестве входа: это другие представления. В примерах отключены внешние diff-помощники и textconv, чтобы результат формировался обычным сравнением Git.
Режим –no-index сравнивает два файла вне репозитория. Его код завершения 1 означает обнаруженные различия, поэтому автоматизация не должна бездумно считать любой ненулевой результат аварией. Отдельно обработайте фактическую ошибку команды. Подробная семантика параметров приведена в официальном руководстве git diff.
git diff --no-ext-diff --no-textconv --output=working.patch -- src/
git diff --cached --no-ext-diff --no-textconv --output=staged.patch
git diff --no-ext-diff --no-textconv HEAD~1 HEAD --output=release.patch
git diff --no-index --no-ext-diff --no-textconv --output=files.patch -- before.txt after.txt
Встраивание в PHP-проект или административную панель
Для PHP-сайта браузерная библиотека подключается как обычная статика. Backend выбирает две разрешённые пользователю версии и отдаёт текст либо готовый patch. Не принимайте произвольный путь к файлу сервера из query-параметра: вместо него используйте идентификаторы сущностей и проверку доступа. Например, versionId должен принадлежать документу, который пользователь вправе читать.
В интерфейсе рядом со сравнением полезно показывать автора, дату, название версии и назначение изменения. Кнопка «Сохранить» должна относиться к конкретной редакции, которую человек только что просмотрел. Если другой участник успел изменить документ, сервер проверяет версию и предлагает сравнить заново. Красивое представление без контроля конкурирующих изменений не решает задачу согласования.
Для простого локального просмотра готовой мастерской можно использовать встроенный сервер PHP. Он обслуживает public и не вычисляет diff на backend. Для постоянного размещения применяйте обычную конфигурацию веб-сервера. Встраивание через iframe изолирует стили мастерской от темы сайта и позволяет дать ссылку на большое отдельное окно.
php -S 127.0.0.1:8185 -t public
VPS: публикуем собранную статику через Caddy
В каталоге сайта должны находиться содержимое public, локальные стили и JavaScript-модули. Для этой мастерской не требуется PHP-FPM или постоянно работающий Node-процесс. Caddy может обслуживать готовые файлы по HTTPS. Пример ниже предполагает уже настроенный домен, доступные сетевые порты и вашу обычную процедуру управления веб-сервером.
Перед заменой действующего сайта подготовьте отдельный каталог релиза и проверьте его содержимое. Пути к модулям и Worker должны оставаться согласованными: копирование только index.html оставит страницу без работающего сравнения. В нашей сборке используются расширения .js, чтобы хостинг отдавал модули с JavaScript MIME. Если меняете структуру URL, проверьте загрузку Worker после публикации.
Сначала выполните caddy validate, затем примените изменение штатным способом вашей установки. Подробности HTTPS и reverse proxy разобраны в статье о Caddy. Демонстрационный домен в конфигурации нужно заменить своим; этот фрагмент не подключает ваш сервер автоматически.
review.example.com {
root * /srv/diff2html/public
encode zstd gzip
file_server
}
caddy validate --config /etc/caddy/Caddyfile
# After successful validation and review:
sudo systemctl reload caddy
Почему сравнение выполняется в Web Worker
Поиск различий может занимать заметное время, особенно на больших похожих текстах. Выполнение в главном потоке замораживает кнопки и прокрутку. В нашей мастерской отдельный Worker строит patch и HTML, а основной поток управляет формой и показывает результат. Это позволяет остановить вычисление и сохранить отзывчивость интерфейса.
Ограничения применяются до и после сравнения: размер полей, число строк, число файлов и объём готовой разметки. Строка длиннее 20 000 символов или превышение порога отображения приводит к понятной ошибке; скрытое содержимое не считается совпадением версий. Через пять секунд Worker завершается. Также ограничены время и число изменений при построении patch в jsdiff. Эти пределы описывают возможности учебной страницы, а не универсальный максимум библиотеки: в своём приложении их выбирают по устройствам, объёму документов и допустимому ожиданию.
При изменении полей или переключении сцены прежний Worker прекращается, а результат становится недействительным. Каждому запуску соответствует собственный номер; поздний ответ старой задачи не должен заменить новый. Это особенно важно для интерфейса, где человек быстро меняет примеры или редактирует текст во время расчёта.
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
const timeout = setTimeout(() => worker.terminate(), 5000);
worker.postMessage({ mode: 'pair', before, after, filename, context: 3 });
// In a real UI also clear timeout and discard stale request results.
HTML, секреты и границы доверия
Показ исходного кода в браузере требует аккуратности: текст, который нужно прочитать, не должен исполняться. diff2html экранирует содержимое строк, а наша мастерская дополнительно очищает сгенерированную разметку DOMPurify перед вставкой. В отчёте запрещены скрипты, iframe и изображения из входного текста. Исходные примеры и сообщения об ошибках выводятся как текст.
Эта обработка не превращает журнал изменений в безопасное место для секретов. Пароль, удалённый из конфигурации, всё равно присутствует в старой версии и попадёт в patch. Для реальной административной панели нужна политика того, какие документы вообще разрешено сравнивать и скачивать. Проверка доступа выполняется до передачи версий браузеру.
О возможностях и ограничениях очистки HTML подробнее рассказано в мастерской DOMPurify. Здесь она используется как дополнительный слой на конкретном HTML-выводе. Полный аудит всего приложения, прав пользователей и хранения версий остаётся отдельной задачей, которую наличие одной библиотеки не закрывает.
Пробелы, переносы строк и текстовое сравнение
В Windows часто встречается CRLF, а в Linux — LF. Если одна версия файла полностью изменила переносы строк, отчёт может оказаться гораздо больше ожидаемого. Сначала выясните, это реальное содержательное изменение или преобразование формата. Нормализацию лучше выполнять осознанно и одинаково для обеих сторон, сохраняя исходные файлы отдельно.
Похожая проблема возникает с форматированием JSON, пробелами и перестановкой строк. Полное игнорирование пробелов может скрыть существенные изменения в языках, где отступы имеют смысл. Поэтому наша мастерская не включила универсальное «исправить всё автоматически». Вы видите разницу введённых текстов, а отдельный подготовительный этап можете добавить под конкретный тип документа.
Если файл не заканчивается переводом строки, patch содержит соответствующую служебную отметку. Не удаляйте её при копировании ради красоты: она относится к точному представлению исходного текста. В тестах проекта проверяется восстановление русской строки без конечного перевода строки, чтобы экспорт не терял это отличие незаметно.
Тесты: проверяем результат, а не только красивую подсветку
Для каждого из восьми сценариев тест строит patch и применяет его к исходной версии средствами jsdiff. Полученный текст должен в точности совпасть с ожидаемой новой версией. Это проверяет содержательное соответствие сравнения. Отдельно проверяются число файлов, отсутствие изменений, нулевой контекст, Unicode и строка без конечного переноса.
Ошибочные входы тоже входят в проверку: слишком большой текст, нулевой байт, недопустимое имя, неподдерживаемые combined и binary diff, неправильные счётчики hunk. Тест HTML проверяет, что строка с тегами остаётся видимым исходником и не создаёт выполняемые элементы. CLI проверяется реальным запуском, включая отказ перезаписывать существующий отчёт.
Браузерная проверка дополняет эти тесты: нужно нажать кнопки, переключить сценарий, изменить текст, скачать файл и открыть результат. Проверка DOM в Node.js не доказывает правильную мобильную вёрстку или загрузку Worker с реального хостинга. Поэтому локальная логика, чистая установка и опубликованная мастерская проверяются как разные этапы.
npm test
npm run build
npm audit --audit-level=low
Как развить мастерскую под свой сервис
Начните с одного понятного сценария: сравнение редакций шаблона, настроек интеграции или инструкции. Определите, где хранятся версии, кто имеет право их читать и какая операция следует после просмотра. Например, для согласования нужна кнопка подтверждения конкретной версии, а для отчёта релиза достаточно экспорта и ссылки на исходный commit.
Затем добавляйте функции, которые помогают этому сценарию: фильтр по файлам, переход к следующему изменению, комментарий к строке, сохранённые замечания. Не переносите интерфейс полноценной системы code review в маленькую форму без необходимости. Пользователю должно быть понятно, какую пару версий он смотрит, что именно изменилось и что произойдёт после его действия.
Исходный проект можно использовать как основу под MIT с сохранением уведомлений лицензий. Зависимости сохраняют собственные лицензии; полный набор уведомлений включён в архив. Для продолжения используйте официальные руководства и зафиксированные версии примера, а обновление библиотеки сопровождайте повторной проверкой формата patch, экспорта и поведения интерфейса.


