Проєктуємо систему JavaScript-плагінів

8 хв. читання

У світі вебінструментів та фреймворків не обійтися без плагінів. У Wordpress є плагіни. У jQuery є плагіни. У Gatsby, Eleventy та Vue вони теж є.

У бібліотеках та фреймворках плагіни існують не просто так: з ними безпечніше додавати нові функції і вони ж роблять архітектуру масштабованою. Тож проєкт отримує фічі та збільшує спільноту — і це без жодної додаткової підтримки.

То як саме створити систему плагінів? Розберемось на практиці — а для цього створимо власну систему на JavaScript.

Автор використовує термін «плагін» для поняття, яке ще називають «розширення» та «модуль».

Власна система плагінів

Для прикладу розглянемо простий застосунок BetaCalc. Це мінімалістичний калькулятор на JavaScript, в який інші розробники можуть додавати свої кнопки. Напишемо початковий код:

// код застосунку
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};

// Використання
betaCalc.setValue(3); // => 3
betaCalc.plus(3);     // => 6
betaCalc.minus(2);    // => 4

Для простоти вся реалізація зібрана у звичайному літералі об'єкта. Перевіряємо його роботу за допомогою викликів console.log.

Зараз функціональність калькулятора дуже обмежена. У нас є метод setValue, який приймає два аргументи та виводить їх на умовний екран. Методи plus та minus виконують відповідні дії для введених значень.

Розширимо функціональність мінізастосунку через систему плагінів.

Створення системи плагінів

Почнемо з методу register, який реєструє плагіни у застосунку BetaCalc. Він приймає сторонній плагін, а саме його функцію exec, та додає її до методів калькулятора:

const betaCalc = {
  // ...код застосунку

  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};

Використаємо щойно створений метод, аби розширити наш калькулятор кнопкою піднесення до квадрата.

// Оголошуємо плагін
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};

// Реєструємо його
betaCalc.register(squaredPlugin);

Більшість систем плагінів має два складники:

  1. код для виконання;
  2. метадані (назва, опис, номер версії, залежності тощо).

У створеному нами плагіні функція exec відповідає за код, а параметр name — за метадані. Коли плагін реєструється, функція exec стає частиною об'єкта BetaCalc і отримує доступ до this.

Тепер наш калькулятор отримав нову кнопку, яку можна одразу протестувати.

betaCalc.setValue(3); // => 3
betaCalc.plus(2);     // => 5
betaCalc.squared();   // => 25
betaCalc.squared();   // => 625

У створеної нами системи багато переваг. Новий плагін — простий літерал об'єкта, який можна передати у функцію, що реєструє їх. Тобто плагіни можна завантажити через npm та імпортувати як модулі ES6. А простота розповсюдження для такої системи дуже важлива.

Однак в цього підходу є і недоліки.

Ми даємо нашому плагіну доступ до this об'єкта BetaCalc, тож він може записувати/зчитувати інформацію всього об'єкта. Так можна зачепити currentValue, а це напряму вплине на роботу застосунку. Якщо плагін може перезаписати внутрішню функцію (наприклад, setvalue), то застосунок та інші його плагіни будуть працювати некоректно.

Такий підхід порушує принцип відкритості/закритості (пер. open-closed principle): сутності в програмному коді повинні бути відкритими для розширення, але закриті для модифікації.

Ще один недолік — функція нового плагіну додає «сторонні ефекти». Звичайно, деколи без них не обійтись, однак краще уникати ситуацій, коли, наприклад, декілька плагінів можуть впливати на той самий стан. З більш функціональним підходом система стане безпечнішою та передбачуванішою.

Покращуємо архітектуру плагінів

Спробуймо покращити архітектуру системи плагінів, аби їх використання не шкодило основному коду:

// калькулятор
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) => currentVal + addend,
    'minus': (currentVal, subtrahend) => currentVal - subtrahend
  },

  plugins: {},    

  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },

  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// наш плагін
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};

betaCalc.register(squaredPlugin);

// використання калькулятора
betaCalc.setValue(3);      // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625

Ми зробили декілька вагомих змін.

По-перше, відділили плагіни від основних методів застосунку (на зразок plus та minus), розмістивши їх у власному об'єкті. Так наша система стала більш безпечною, адже плагіни, що мають доступ до this, не можуть отримати доступ до властивостей об'єкта. Їм відомі хіба що властивості betaCalc.plugins.

По-друге, ми реалізували метод press, який шукає функцію кнопки за назвою та викликає її. Тепер, коли ми викликаємо плагінову функцію exec, то передаємо їй поточний стан калькулятора (currentValue) та очікуємо повернення нового значення.

Можна помітити, що створений метод press перетворює всі кнопки калькулятора на чисті функції. Вони приймають значення, виконують над ним операцію та повертають результат. У такого підходу є кілька переваг:

  • спрощується API;
  • код простіше тестувати (як код самого застосунку, так і плагінів);
  • у системи менше залежностей, отже вони слабше зв'язані.

Тепер архітектура системи більш обмежена, якщо порівняти з першим прикладом, але такі зміни на краще. Ми встановили обмеження для авторів плагінів, аби вони могли робити лише ті зміни, які ми хочемо.

Зараз плагіни нашого калькулятора можуть виконувати лише операції над currentValue. Якби автор плагіну хотів додати ще функцій (кнопки запам'ятовування чи, скажімо, спосіб збереження історії), він не міг би цього зробити.

Тож доступи, які ви даєте авторам плагіну, залежать лише від особливостей вашого застосунку. Пам'ятайте, що надмірний доступ може негативно вплинути на стабільність проєкту, а надто суворі обмеження заважають плагінам виконувати свою роботу.

Що далі?

Насправді в нашому проєкті є що вдосконалювати. Ми можемо додати систему обробки помилок, яка сповіщатиме, коли автори забули визначити назву чи повернути значення. Тут вже варто підійти до системи з боку тестувальника і проаналізувати, як наша система може зламатись і як обробляти такі випадки.

Ми також могли б розширити функціональність плагіну. Наразі він вміє лише додавати кнопку до застосунку. Можна реалізувати й плагін реєстрації колбеків для особливих подій життєвого циклу застосунку (наприклад, коли калькулятор збирається показати значення). Або ж виділити місце для збереження стану між декількома діями.

Вдосконалити можна й сам процес реєстрації плагінів. А що, як плагін можна було б зареєструвати з певними початковими налаштуваннями? Чи стали б плагіни тоді більш гнучкими? А якби розробник плагіну хотів би зареєструвати цілий набір кнопок замість однієї? Необхідно продумати, які зміни знадобилися б для підтримки таких функцій.

Системи плагінів в продакшені

Для нашого прикладу ми спеціально підібрали просту архітектуру застосунку та системи плагінів. Якщо ваш застосунок значно більший, ви, напевно, розглядатимете дещо інші архітектури.

Як варіант, можна розглянути відомі системи плагінів. В екосистемі JavaScript це jQuery, Gatsby, D3, CKEditor тощо.

Для розширюваної системи плагінів варто також знатися на патернах проєктування у JavaScript (на цю тему є книга Addy Osmani). Кожен патерн пропонує різний інтерфейс та різний рівень зв'язаності, тому ви можете обрати архітектуру з потрібними властивостями. Якщо ви добре розбиратиметесь в різних архітектурах, то легко знайдете найкращий варіант для кожного проєкту.

Для правильних архітектурних рішень патерни не обов'язкові. Існує багато принципів проєктування, які допоможуть вам побудувати гнучку та розширювану систему. Наприклад, принцип відкритості-закритості, згаданий вище, закон Деметри, впровадження залежностей.

Здається, що треба знати безліч речей, аби створити власну систему плагінів. Але не все так страшно. Краще добре спроєктувати систему із самого початку, ніж потім змінювати кожен плагін окремо.

Висновок

Аби створити підтримувану, надійну та розширювану архітектуру з нуля, варто добре постаратись. Треба врахувати безліч факторів: чи достатньо система проста, чи потужна, чи працюватиме в довгостроковій перспективі.

Однак ці зусилля варті того. Надійна система плагінів — потужний та гнучкий інструмент, який допомагає всім. Розробники можуть вільно розв'язувати свої проблеми, а користувачі отримують більше функцій. Ви ж можете створити екосистему навколо проєкту, яка зростатиме разом з ком'юніті. Куди не глянь — суцільні плюси.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація