Google Apps Script — Сам себе триггер в библиотеке

Google Apps Script — Сам себе триггер в библиотеке


Статья Михаила Смирнова, написанная для канала t.me/google_sheets

Чат канала: t.me/google_spreadsheets_chat

Другие статьи Миши, написанные для нашего канала: t.me/google_sheets/1203


Здравствуйте, товарищи!


Сегодня мы посмотрим, как ловко создать триггер изнутри библиотеки на внутреннюю функцию библиотеки в Google Apps Script.


Фото: © VLADISLAV ROGOZOV
На фото Леонид Иванович Рогозов, советский врач-хирург, участник 6-й Советской антарктической экспедиции, в 1961 году сделавший сам себе операцию по поводу острого аппендицита.


Зачем оно нужно, кто-то и сходу поймёт, им достаточно посмотреть пару строчек кода (собственно, можете глянуть), а для остальных товарищей ниже небольшая предыстория.


Предыстория

Я написал библиотеку, которая что-то делает в с таблицами. Часть функциональности библиотеки работает несколько нестабильно: в большинстве случаев всё ок, но бывают ошибки, которые непонятно, как исправлять. Как известно у Гугла баги, это их ошибки, а я-то, как известно, красавчик и ни при чём.


Пришлось нагородить костыль: написал функцию, которая сбрасывает всё в рабочее состояние, повесил её на кнопку в меню. Но это кто-то должен заметить, что оно перестало работать, призвать начальника, а он кнопку нажмёт (от греха только ему эта кнопку доступна).


Короче, так себе решение проблемы с призывом начальника. Лучше было бы в начале выполнения создать time-based триггер, который через пару минут сам запустит лечебную функцию. Если всё ок, и ничего не упало, то триггер в конце выполнения удалим, а если упало, то триггер отработает и всё полечит.


Чтобы всю логику, которая есть вокруг создания и удаления триггера не тащить в проект, пусть живёт в библиотеке, рядом с теми функциями чьи последствия будет лечить. Вот тут-то и возник вопрос, как изнутри библиотеки создать триггер на функцию, которая тоже живёт внутри библиотеки?


Про библиотеки (Google Apps Script Library)

Вводная статья про библиотеки уже была – https://telegra.ph/Google-Apps-Script-Library--Biblioteki-v-Gugl-Skriptah-11-02


Про триггеры

Есть простые триггеры (simple triggers): достаточно назвать функцию правильным именем, и она будет в соответствующий момент выполняться.


Например:

function onOpen(e) {
  Logger.log('Документ кто-то открыл');
}


При открытии таблицы (если проект привязан к таблице), функция отработает, в лог будет добавлено сообщение.


Такие триггеры в библиотеках бесполезны. Едем дальше.


Есть устанавливаемые триггеры (installable triggers), которые нужно руками создать. Нужно выбрать функцию, которая будет вызвана триггером и настройки триггера:


1. Переходим в раздел с триггерами
2. Жмём, чтобы создать новый
3. Выбираем функцию из проекта
4, 5, 6. Ставим триггер, пусть раз в час запускается

Он появился в списке:



Но тут есть неудобство и одна проблема:

  • создали руками, а надо автоматом, из кода
  • выбрать можно только функции из проекта, из библиотеки − нельзя


Создаём триггер программно

Проблему и неудобство решим одним махом. Создаём триггер, который сработает не раньше, чем через 1 минуту:

ScriptApp.newTrigger('testlib.heal')
  .timeBased()
  .after(1)
  .create();


Если создавать триггер программно, можно указать функцию до которой надо добираться через точку: метод объекта, метод подобъекта объекта или ещё как спрятанную. В нашем случае − это функция библиотеки heal(), которая занимается лечением, к которой мы доступаемся testlib.heal.


Прятать функцию в библиотеке, добавляя _, (назвав heal_()) нельзя, триггер её не найдёт.


Тут ещё небольшой бонус: можно создавать одноразовые триггеры, которые сработают через заданное количество минут. Вручную тоже можно, но придётся указать точное время запуска.


— Ок, а как из библиотеки создать?

— Вот так, как выше, и создать. Название функции знаем, название библиотеки тоже − готово.

— А если библиотека импортировалась под другим именем?, — спросите вы.

— Смотрю, тебя не проведёшь! — скажу я.


Как из библиотеки узнать, как её обозвали?

Это сделать можно, сейчас расскажу. Есть важный момент: некоторые глобальные объекты проекта, который использует библиотеку, шарятся с библиотекой, другие − нет.


К счастью ScriptApp из тех, что шарятся.


Добавим в код нашей библиотеки, в самое начало следующее:

// This library's Script Id
const SCRIPTID_ = '1qYE4E5tQ7FwrgANl6CRZ5nXK896GFX1TVWsnzP1C9Z3-y0qzmma9PGHt';
// The name used the project that imported this library
const LIBSYMBOL_ = JSON.parse(ScriptApp.getResource('appsscript').getDataAsString())
  .dependencies
  .libraries
  .find((lib) => lib.libraryId === SCRIPTID_)
  .userSymbol;


SCRIPTID_ − это переменная, в которую мы положили Id библиотеки, по нему она подключается.

LIBSYMBOL_ − тут будет название библиотеки, которое назначено в проекте, который её импортировал.


Достаём его с помощью недокументированной (о, да!) функции ScriptApp.getResource('resourceName'), которая возвращает содержимое ресурса (одного из файлов проекта) в виде blob. resourceName − имя файла в проекте, без расширения. В нашем случае мы достаём содержимое манифеста appsscript.json, в котором содержатся настройки проекта, в том числе и данные про подключённые библиотеки:


Используем имя superLib, хотя родное – testlib


Далее мы из blob получаем текст (getDataAsString()), парсим его (JSON.parse(...)) − получаем объект, в котором потом находим запись, соответствующую нашей библиотеке, и берём из неё параметр userSymbol.


Вот теперь можем не беспокоиться, что кто-то библиотеку импортирует под другим именем:

ScriptApp.newTrigger(`${LIBSYMBOL_}.heal`)
    .timeBased()
    .after(1)
    .create();


Всё вместе

Что в итоге?


Код библиотеки (можно импортировать себе, поиграться, скопировать код):

// This library's Script Id
const SCRIPTID_ = '1qYE4E5tQ7FwrgANl6CRZ5nXK896GFX1TVWsnzP1C9Z3-y0qzmma9PGHt';
// The name used the project that imported this library
const LIBSYMBOL_ = JSON.parse(ScriptApp.getResource('appsscript').getDataAsString())
  .dependencies
  .libraries
  .find((lib) => lib.libraryId === SCRIPTID_)
  .userSymbol;

Logger.log(`This library is imported with the name "${LIBSYMBOL_}".`);

function createTrigger_() {
  ScriptApp.newTrigger(`${LIBSYMBOL_}.heal`)
    .timeBased()
    .after(1)
    .create();
}

function heal(e) {
  // Find the trigger that fired this function in the list of all triggers
  const thisTimeBasedTrigger = ScriptApp.getProjectTriggers()
    .find((trigger) => trigger.getUniqueId() === e.triggerUid);

  // If the trigger was found
  if (thisTimeBasedTrigger != null) {
    // Delete the trigger
    ScriptApp.deleteTrigger(thisTimeBasedTrigger)
  }

  // Actual healing
  Logger.log('Healing!');
}

function work() {
  // Create trigger just in case
  createTrigger_();

  // Actual work
  Logger.log('Working!');

  // Emulate crash
  throw Error('что-то пошло не так...');
}


Функцию work() мы будем вызывать из нашего проекта, который использует библиотеку. Она создаёт триггер, который сработает через минуту, изображает бурную деятельность (пишет в лог одну строчку), потом падает, выкинув ошибку.


Функция heal(e) в этом примере чуть хитрее, хоть и тут заглушка вместо реальной деятельности.

  • Во-первых, она имеет параметр e − это объект события, который передаётся триггером.
  • Во вторых, мы прибираемся за собой. Используем параметр e.triggerUid, который содержит Id триггера, который вызвал функцию, чтобы найти и удалить этот триггер. Триггер-то одноразовый, зачем ему висеть после того, как отработает?


Код в проекте, который использует библиотеку:

function useLib() {
  superLib.work();
}


Просто запускаем функцию из библиотеки testlib, которую мы импортировали под именем superLib (оно ей больше подходит).


Попробуем запустить:


1. Это сказала библиотека при инициализации, правильно узнала своё имя
2. Это мы типа работаем
3. А это мы сломались


Триггер был создан:



Через минуту запустил нужную функцию:



По ходу функция удалила триггер, который её запустил:



Замечания

Пара слов про недокументированную функцию ScriptApp.getResource('...'). Я её вчера нашёл, разглядывая содержимое объектов: так Object.keys(ScriptApp) и так Object.getOwnPropertyNames(ScriptApp).


Что делает, не понял, загуглил. На Stackoverflow пишут (аж в 2012 году! куда милиция смотрит?!), что выдаёт содержимое файла проекта.


В сентябре в отличном чате по теме её находил Максим Стоянов, который использовал для чего-то другого, тоже очень полезного, я пока не разобрался. Максим выражал надежду, что скоро Гугл нам про неё расскажет, но оказывается прошло не меньше 9 лет, а они всё молчат. :)


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


Попутно было замечено, что можно скопировать в проекте appscript.json, и у вас будет .json в проекте. Штатно можно создать только файл скрипта .gs или HTML код .html. Зачем вам .json в проекте, не знаю, но может пригодится?


Правда, .json этот должен иметь валидную структуру манифеста. Вроде бесполезная штука.



На этом всё. Спасибо за внимание.


Ссылки

Google Apps Script Library

  • Про библиотеки
  • Очень важно про то, какие части проекта, который использует библиотеку, видны в библиотеке

Триггеры

  • Simple triggers и installable triggers
  • newTrigger() и TriggerBuilder – создаём триггер программно
  • Содержимое событий триггеров

Google Apps Script

  • Про указание функций, до которых надо добираться через точку
  • И вообще про используемый в скриптах движок V8
  • Недокументированная функция ScripApp.getResource(): на Stackoverflow и от Максима Стоянова
  • Про структуру appsscipt.json

Статья Михаила Смирнова, написанная для канала t.me/google_sheets

Чат канала: t.me/google_spreadsheets_chat

Другие статьи Миши, написанные для нашего канала: t.me/google_sheets/1203

Report Page