Создать APK бесплатно

JavaScript bridge API

Фоновые задания

Работа, которая продолжается после закрытия приложения: надёжное автосохранение, периодическая синхронизация.

workCancel мост#

window.WebToApk.workCancel(id: String)

Разрешение не требуется; результаты передаются как `appmint:work` / window.onAppMintWork.

Описывается в своей группе, Фоновые задания (WorkManager): надёжное автосохранение + периодическая нативная синхронизация, а не отдельно.

Пример

Отменяет фоновое задание (разовое или повторяющееся) по его идентификатору и удаляет сохранённый запрос.

Возвращает: ничего напрямую. Событие appmint:work (и window.onAppMintWork) отвечает { id, status: 'cancelled' }. Требуется: ничего.

function stopSync() {
  if (!(window.WebToApk && window.WebToApk.workCancel)) return;
  WebToApk.workCancel('sync');
}

window.addEventListener('appmint:work', function (e) {
  if (e.detail.id === 'sync' && e.detail.status === 'cancelled') {
    document.getElementById('sync-status').textContent = 'Background sync is off';
  }
});

Отбросьте черновик: отмените его автосохранение, которое ещё не отправлено, затем очистите редактор, когда Android подтвердит:

function discardDraft() {
  if (!(window.WebToApk && window.WebToApk.workCancel)) { clearEditor(); return; }
  function onWork(e) {
    if (e.detail.id !== 'autosave' || e.detail.status !== 'cancelled') return;
    window.removeEventListener('appmint:work', onWork);
    clearEditor();
  }
  window.addEventListener('appmint:work', onWork);
  WebToApk.workCancel('autosave');
}

При выходе из аккаунта отмените все задания этого приложения, используя идентификаторы, которые сообщает workList:

function cancelAllJobs() {
  if (!(window.WebToApk && window.WebToApk.workList && window.WebToApk.workCancel)) return;
  const requestId = 'signout-' + Date.now();
  function onWork(e) {
    if (String(e.detail.requestId) !== requestId) return;
    window.removeEventListener('appmint:work', onWork);
    (e.detail.jobs || []).forEach(function (job) {
      WebToApk.workCancel(job.id);                      // each answers { id, status: 'cancelled' }
    });
  }
  window.addEventListener('appmint:work', onWork);
  WebToApk.workList(requestId);
}

Примечания: отвечает cancelled даже если задания с таким идентификатором не было. Автосохранение, которое ещё не отправлено, отбрасывается, поэтому отменяйте, только если вы действительно не хотите его доставки.

workEnqueueOnline мост#

window.WebToApk.workEnqueueOnline(id: String, url: String, method: String, headersJson: String, payload: String)

Разрешение не требуется; результаты передаются как `appmint:work` / window.onAppMintWork.

Описывается в своей группе, Фоновые задания (WorkManager): надёжное автосохранение + периодическая нативная синхронизация, а не отдельно.

Пример

Передаёт Android один HTTP-запрос для отправки, когда телефон онлайн — даже если пользователь закроет приложение или перезагрузит телефон. Используйте для надёжного автосохранения.

Возвращает: ничего напрямую. Ответ приходит как событие appmint:work (и window.onAppMintWork(detail), если вы её определили): { id, status: 'enqueued' } или { id, status: 'failed', reason } с причиной invalid_arguments (пустой идентификатор или URL, не начинающийся с http) или enqueue_failed. Требуется: ничего.

Надёжное автосохранение: тот же идентификатор заменяет ещё не отправленное сохранение, так что побеждает самое новое состояние.

function autoSave(state, token) {
  if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) {
    showMessage('Offline auto-save works in the installed app only.');
    return;
  }
  WebToApk.workEnqueueOnline(
    'autosave',                                        // your job id
    'https://api.example.com/save',                    // http(s) URL
    'POST',                                            // POST | PUT | GET ...
    JSON.stringify({ Authorization: 'Bearer ' + token }), // headers as a JSON string
    JSON.stringify(state)                              // body as a string ('' for none)
  );
}

window.addEventListener('appmint:work', function (e) {
  const d = e.detail;
  if (d.id !== 'autosave') return;
  if (d.status === 'enqueued') showSavedBadge('Will sync when online');
  if (d.status === 'failed') showMessage('Could not queue the save: ' + d.reason);
});

Ставьте в очередь каждую отправку формы как отдельное задание (уникальный идентификатор на каждую отправку, чтобы вторая не заменила первую) и дожидайтесь ответа Android:

function queueFeedback(form) {
  return new Promise(function (resolve, reject) {
    if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) {
      reject(new Error('not_in_app'));
      return;
    }
    const id = 'feedback-' + Date.now();
    function onWork(e) {
      if (e.detail.id !== id) return;                  // another job's answer
      window.removeEventListener('appmint:work', onWork);
      if (e.detail.status === 'enqueued') resolve(id);
      else reject(new Error(e.detail.reason));         // invalid_arguments | enqueue_failed
    }
    window.addEventListener('appmint:work', onWork);
    WebToApk.workEnqueueOnline(id, 'https://api.example.com/feedback', 'POST', '{}',
      JSON.stringify({ rating: form.rating.value, text: form.text.value }));
  });
}

Сохраните заметку как обычный текст через PUT; если задать свой Content-Type, JSON по умолчанию не добавляется:

function saveNote(noteId, text) {
  if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) return;
  WebToApk.workEnqueueOnline(
    'note-' + noteId,                                  // same note, same id: only the newest text is sent
    'https://api.example.com/notes/' + encodeURIComponent(noteId),
    'PUT',
    JSON.stringify({ 'Content-Type': 'text/plain; charset=utf-8' }),
    text
  );
}

document.getElementById('note').addEventListener('change', function (e) {
  saveNote(e.target.dataset.noteId, e.target.value);
});

Примечания: enqueued означает, что Android принял задание, а не что сервер получил запрос; события о доставке нет. Правила доставки: ответ 2xx завершает задание; ответ 4xx останавливает его (без повтора); сетевая ошибка или 5xx — повтор с растущими задержками, до 8 попыток. Content-Type: application/json добавляется, если вы не задали свой. GET не отправляет тело. JavaScript страницы в фоне не выполняется — работает только этот нативный запрос.

workList мост#

window.WebToApk.workList(requestId: String)

Разрешение не требуется; результаты передаются как `appmint:work` / window.onAppMintWork.

Описывается в своей группе, Фоновые задания (WorkManager): надёжное автосохранение + периодическая нативная синхронизация, а не отдельно.

Пример

Перечисляет фоновые задания, которые это приложение поставило в очередь или запланировало.

Возвращает: ничего напрямую. Событие appmint:work (и window.onAppMintWork) отвечает { requestId, jobs: [{ id, periodic, url, createdAt, pendingDelivery }] }. pendingDelivery равно true, пока разовое задание ещё не доставлено (и всегда для периодического). Требуется: ничего.

Небольшая обёртка на Promise, сопоставляющая ответ по requestId:

function listJobs() {
  return new Promise(function (resolve) {
    if (!(window.WebToApk && window.WebToApk.workList)) { resolve([]); return; }
    const requestId = 'jobs-' + Date.now();
    function onWork(e) {
      if (String(e.detail.requestId) !== requestId) return;   // not our answer
      window.removeEventListener('appmint:work', onWork);
      resolve(e.detail.jobs || []);
    }
    window.addEventListener('appmint:work', onWork);
    WebToApk.workList(requestId);
  });
}

listJobs().then(function (jobs) {
  const unsent = jobs.filter(function (j) { return !j.periodic && j.pendingDelivery; });
  document.getElementById('unsent').textContent =
    unsent.length ? unsent.length + ' change(s) waiting for network' : 'All changes saved';
});

Покажите все задания на экране настроек с временем постановки в очередь:

window.addEventListener('appmint:work', function (e) {
  if (e.detail.requestId !== 'settings-jobs') return;
  const list = document.getElementById('jobs');
  list.innerHTML = '';
  (e.detail.jobs || []).forEach(function (job) {
    const li = document.createElement('li');
    li.textContent = job.id + (job.periodic ? ' (repeats)' : job.pendingDelivery ? ' (waiting)' : '') +
      ' - since ' + new Date(job.createdAt).toLocaleString();
    list.appendChild(li);
  });
});

if (window.WebToApk && window.WebToApk.workList) WebToApk.workList('settings-jobs');

При открытии приложения установите переключатель «Фоновая синхронизация» в зависимости от того, зарегистрировано ли ещё повторяющееся задание 'sync':

window.addEventListener('appmint:work', function (e) {
  if (e.detail.requestId !== 'startup') return;
  const on = (e.detail.jobs || []).some(function (j) { return j.id === 'sync' && j.periodic; });
  document.getElementById('sync-toggle').checked = on;
});

document.addEventListener('DOMContentLoaded', function () {
  if (window.WebToApk && window.WebToApk.workList) WebToApk.workList('startup');
});

Примечания: разовое задание исчезает из списка, когда оно доставлено, отклонено ответом 4xx или оставлено после 8 попыток. Числовой requestId может вернуться как число, поэтому сравнивайте с String(...).

workSchedulePeriodic мост#

window.WebToApk.workSchedulePeriodic(id: String, url: String, method: String, headersJson: String, payload: String, intervalMinutes: Int)

Разрешение не требуется; результаты передаются как `appmint:work` / window.onAppMintWork.

Описывается в своей группе, Фоновые задания (WorkManager): надёжное автосохранение + периодическая нативная синхронизация, а не отдельно.

Пример

Планирует повторяющийся HTTP-запрос, который Android отправляет в фоне (когда есть сеть), даже когда приложение закрыто. Подходит для запуска синхронизации или периодического сигнала.

Возвращает: ничего напрямую. Ответ приходит как событие appmint:work (и window.onAppMintWork(detail)): { id, status: 'scheduled', intervalMinutes } или { id, status: 'failed', reason } с причиной invalid_arguments или schedule_failed. Требуется: ничего.

function startBackgroundSync(userId) {
  if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) {
    showMessage('Background sync works in the installed app only.');
    return;
  }
  WebToApk.workSchedulePeriodic(
    'sync',                                   // job id (scheduling again updates it)
    'https://api.example.com/sync',
    'POST',
    JSON.stringify({ 'X-User': userId }),     // headers as a JSON string
    JSON.stringify({ userId: userId }),       // the same body is sent every time
    30                                        // minutes; values under 15 become 15
  );
}

// Optional page hook instead of addEventListener:
window.onAppMintWork = function (d) {
  if (d.id === 'sync' && d.status === 'scheduled') {
    console.log('sync every', d.intervalMinutes, 'minutes');
  }
  if (d.status === 'failed') console.warn('job', d.id, 'failed:', d.reason);
};

Сигнал раз в час: GET не отправляет тело, поэтому передайте пустую строку, и '{}', когда заголовки не нужны:

function startHeartbeat(deviceId) {
  if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) return;
  WebToApk.workSchedulePeriodic(
    'heartbeat',
    'https://api.example.com/ping?device=' + encodeURIComponent(deviceId),
    'GET',
    '{}',
    '',
    60
  );
}

Дайте пользователю выбрать, как часто синхронизировать, и покажите интервал, который Android реально использует (всё меньше 15 превращается в 15):

document.getElementById('sync-every').addEventListener('change', function (e) {
  if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) return;
  const minutes = parseInt(e.target.value, 10);        // e.g. 5, 30, 120
  WebToApk.workSchedulePeriodic('sync', 'https://api.example.com/sync', 'POST', '{}',
    JSON.stringify({ reason: 'periodic' }), minutes);  // same id: updates the existing job
});

window.addEventListener('appmint:work', function (e) {
  const d = e.detail;
  if (d.id !== 'sync') return;
  if (d.status === 'scheduled') {
    document.getElementById('sync-note').textContent = 'Syncing about every ' + d.intervalMinutes + ' minutes';
  } else if (d.status === 'failed') {
    document.getElementById('sync-note').textContent = 'Could not schedule sync (' + d.reason + ')';
  }
});

Примечания: Точный момент выбирает Android; 15 минут — самый короткий интервал, а экономия заряда может задерживать запуски. Тело фиксируется при планировании; чтобы изменить его, запланируйте заново с тем же идентификатором. Ответ 4xx не отменяет периодическое задание; следующий запуск попробует снова. Остановите его через workCancel('sync').