Перейти к содержанию

Асинхронная загрузка и интерфейс анализатора

Cargo и wasm-bindgen описывают будущий браузерный пакет «Меры». Теперь подключим его к странице. Инициализация является асинхронной: браузер должен получить JavaScript и бинарный ресурс, подготовить модуль и только затем вызвать экспорт. Нельзя обращаться к вычислительной функции сразу после появления тега script, рассчитывая, что все ресурсы уже готовы.

Создадим загрузчик с одним обещанием готовности и небольшой интерфейс. На этом этапе вычисление выполняется в основном потоке. В уроке о Worker изменим место работы, сохранив внешний результат. Пока важно отделить состояние загрузки от состояния обработки файла и от предметной ошибки входа.

Одна инициализация на экземпляр страницы

Полный начальный web/engine.mjs загружает сгенерированный модуль динамически. Это позволяет наблюдать ошибку получения ресурса в обычном асинхронном коде:

let pendingEngine;

export function getEngine() {
  pendingEngine ??= loadEngine();
  return pendingEngine;
}

async function loadEngine() {
  const module = await import("./pkg/mera.js");
  await module.default();
  return {
    name: "WebAssembly",
    analyze(text) {
      const values = module.analyze_text(text);
      if (values.length !== 5) throw new Error("Unexpected statistics ABI");
      const [bytes, scalars, lines, nonEmptyLines, maxLineScalars] = values;
      return { bytes, scalars, lines, nonEmptyLines, maxLineScalars };
    }
  };
}

Все вызовы getEngine получают одно обещание. Когда пользователь выбирает несколько файлов последовательно, модуль не инициализируется заново для каждого. Загрузчик возвращает адаптер с понятным методом, а не внутреннее устройство памяти Wasm.

В этом варианте отклонённое обещание тоже сохраняется. Следующий вызов не выполняет скрытую повторную сетевую попытку. Для явной кнопки повторения можно сбросить pendingEngine, но это должно быть отдельным решением интерфейса. Позднее запасной движок позволит продолжить работу без Wasm, сохраняя причину выбора реализации.

Проверка длины массива защищает от несогласованного пакета: новый бинарник и старая JavaScript-обвязка не должны незаметно выдавать неправильные подписи числам. Для полноценного версионирования добавим экспорт версии договора. Здесь сначала показываем простейшую наблюдаемую границу.

Форма без автоматического запуска

Полный каркас web/index.html содержит выбор файла, кнопку и две разные области вывода:

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Мера — анализатор текста</title>
</head>
<body>
  <main>
    <h1>Мера</h1>
    <form id="analyzer">
      <label for="source">UTF-8-файл до 8 MiB</label>
      <input id="source" type="file" accept=".txt,text/plain">
      <button id="analyze" type="submit">Посчитать</button>
    </form>
    <p id="status" role="status"></p>
    <dl id="result"></dl>
  </main>
  <script type="module" src="./page.mjs"></script>
</body>
</html>

Выбор файла не отправляет его на сервер. Действие начинается при отправке формы, а обработчик отменяет обычную навигацию. Ограничение accept облегчает поиск файла в диалоге, но фактическая проверка размера и UTF-8 остаётся в readTextFile из предыдущего урока.

Кнопка имеет type="submit", поэтому форма доступна и через клавиатуру. Поле связано с подписью через for и id. Отдельный статус сообщает ход операции, а dl хранит пары показателей. Небольшая семантика полезнее безымянных контейнеров, оформленных только визуально.

Состояния обработки

Начальный обработчик web/page.mjs использует уже существующие getEngine и readTextFile. Это полный вариант действия; функцию showResult определим следующим блоком:

import { getEngine } from "./engine.mjs";
import { readTextFile } from "./input.mjs";

const form = document.querySelector("#analyzer");
const input = document.querySelector("#source");
const button = document.querySelector("#analyze");
const status = document.querySelector("#status");
const result = document.querySelector("#result");

form.addEventListener("submit", async event => {
  event.preventDefault();
  const file = input.files[0];
  result.replaceChildren();
  input.disabled = button.disabled = true;
  try {
    status.textContent = "Читаем файл и готовим модуль…";
    const text = await readTextFile(file);
    const engine = await getEngine();
    status.textContent = "Вычисляем…";
    const stats = await engine.analyze(text);
    showResult(stats);
    status.textContent = `Готово: ${file.name}; движок ${engine.name}`;
  } catch (error) {
    status.textContent = error instanceof Error ? error.message : String(error);
  } finally {
    input.disabled = button.disabled = false;
  }
});

Файл сохраняется в локальную переменную до ожиданий. Во время операции элементы заблокированы, поэтому человек не заменяет источник посреди простого начального сценария. Это не отмена и не конкурентная обработка: позже Worker позволит сделать более гибкий договор запросов.

Выражение await engine.analyze удобно для одинакового внешнего интерфейса будущего асинхронного движка. Однако оно не превращает синхронный Wasm-вызов в отдельный поток. Если вычисление занимает заметное время, основной поток всё равно занят. Это различие важно: Promise описывает ожидание, а Worker меняет место выполнения.

Безопасный вывод показателей

Список подписей задаём в приложении, а числовые значения выводим как текст. Не используем HTML-строку, составленную из имени файла и результата:

function showResult(stats) {
  const labels = {
    bytes: "Байты UTF-8",
    scalars: "Скалярные значения Unicode",
    lines: "Строки",
    nonEmptyLines: "Непустые строки",
    maxLineScalars: "Максимальная длина строки"
  };
  for (const [key, label] of Object.entries(labels)) {
    const term = document.createElement("dt");
    const value = document.createElement("dd");
    term.textContent = label;
    value.textContent = String(stats[key]);
    result.append(term, value);
  }
}

Для кот\nhi ожидаются значения из первого урока. Имя файла показывается только в статусе через textContent. Пользователь получает единицы измерения рядом с числами и не должен угадывать, что означает поле scalars.

Перед каждым действием старый вывод очищается. Если новый файл неверный, предыдущие числа не остаются под новой подписью. finally возвращает управление форме как после успеха, так и после отказа. Эти небольшие свойства определяют достоверность интерфейса даже при правильно работающем вычислительном модуле.

Повторное действие и наблюдаемый режим

После первого успешного анализа пользователь выбирает второй файл. Повторно загружать модуль нет необходимости, но статистику нужно вычислить заново. Кэш обещания готовности не является кэшем результатов файлов. Эти два слоя легко спутать, если назвать обе переменные просто cache. В загрузчике хранится только подготовленный движок; вход и числа остаются локальными для отдельного действия.

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

Состояние загрузки также нельзя считать прогрессом вычисления. Пока браузер готовит Wasm, строка «Вычисляем 80%» была бы выдумкой: у нас нет измеренного числа обработанных строк. Начальный интерфейс показывает только честные этапы. Если позже будет обработка порциями, реальный прогресс можно получить из числа прочитанных байт или завершённых частей, явно указав его смысл.

Наблюдаемое имя движка полезно для диагностики. Пользователь понимает, какая ветвь сработала, а редактор будущего отчёта не сравнивает JavaScript с ним же, ошибочно назвав один запуск Wasm. Само имя не доказывает скорости и не требует сложного технического интерфейса: достаточно короткой подписи в статусе готовности.

Когда ошибка загрузки сохраняется в обещании, повторный выбор файла не исправит путь к бинарнику. Нужна отдельная попытка подготовки или запасной режим. Не оставляйте форму в бесконечном состоянии «Подождите», если обещание уже отвергнуто. Наш catch показывает отказ, а finally возвращает управление — так человек может решить следующее действие.

Для страницы с несколькими независимыми вычислительными виджетами загрузчик может быть общим модулем, но состояния действий должны оставаться отдельными. Одна ошибка файла не должна очищать результат соседнего виджета. Это ещё одна причина держать подготовку движка отдельно от элементов формы и пользовательского ввода.

Если форма встроена в длинную статью, не переносите фокус на результат автоматически. Сохранённая клавиатурная позиция и сообщение статуса позволяют продолжить чтение, не теряя место в документе.

Загрузчик, форма и адаптер теперь имеют ясные обязанности. В следующем уроке добавим гистограмму и рассмотрим передачу числового массива через границу Wasm. Пока описан ожидаемый интерфейс по исходникам; модуль не компилировался и браузерный сценарий не выполнялся.

Оглавление серии