Це керівництво описує загальну архітектуру, яку ви зустрінете у всіх додатках на Elm. від TodoMVC до dreamwriter.
Ми вивчимо дуже простий спосіб компонування додатки, що представляє собою нескінченно вкладаються блоки. Він сильно покращує модульність, спрощує повторне використання коду і тестування. З його допомогою можна створювати складні додатки, розбиваючи їх на складові частини. Ми почнемо з маленького прикладу і будемо поступово розширювати його, використовуючи ці базові принципи.
Цікаво, що подібна архітектура виникає в Elm природним чином. Дизайн мови сам підводить вас до неї, чи читали ви цей текст чи ні. Я і сам виявив цей патерн просто використовуючи Elm і був шокований його простотою і силою.
Зауваження. Щоб випробувати наведений тут код вам потрібно встановити Elm і форкнуть репозиторій. Кожен приклад має інструкцію як запустити код.
основний прийом
Логіка кожної програми в Elm розбивається на три чітко розділені частини: модель, оновлення та відображення. Ви можете кожен раз починати з цього скелета проекту, а потім поступово заповнювати його подробицями під вашу конкретну задачу.
Весь туториал ми будемо використовувати цей патерн з невеликими змінами і доповненнями.
Приклад №1: Лічильник
Код починається з дуже простої моделі. Нам просто треба відстежувати одне число:
При оновленні моделі теж все дуже просто. Ми визначаємо набір дій, які можуть виконуватися і додаємо їх обробку в функцію update:
Тепер залишилося тільки зробити відображення (view) для нашої моделі. Ми будемо використовувати elm-html для створення HTML, який відобразиться в браузері. Ми створюємо div. що містить в собі: кнопку зменшення, div з поточним значенням лічильника і кнопку збільшення.
Найскладніша частина функції view це Signal.Address. Ми займемося цим в наступній частині, а поки я хочу, щоб ви відзначили, що цей код є повністю декларативним. Ми беремо Model і видаємо якийсь Html. І все. Ні в якому місці ми не займаємося ручним зміною DOM, що відкриває бібліотеці великий простір для оптимізації і навіть значно все прискорює. Це просто здорово. Більш того, функція view це звичайнісінька функція і при її створенні ми можемо користуватися всією потужністю системи модулів Elm, фреймворками для тестування і бібліотеками.
Це і є суть компонування будь-яких додатків в Elm. Всі приклади, які ми далі побачимо, будуть лише невеликими варіаціями цього базвого патерну: модель (Model), оновлення (update), відображення (view).
Відступ: пожвавлення вашого застосування за допомогою сигналів
Тепер розберемо частину коду з Signal.Address.
До цього ми говорили тільки про чисті функціях і незмінних даних. Це здорово, але нам треба також і реагувати на події з зовнішнього світу. В Elm цим займаються сигнали. Сигнал це значення, яке змінюється з часом, що дозволяє нам говорити про те, як буде змінюватися наша модель.
В принципі всі програми будуть мати цей невеликий шматочок коду, який обслуговує весь додаток. У прикладі №1 він виглядає ось так:
Хочу звернути вашу увагу на кілька деталей:
- Ми починаємо з 0 в якості стартового значення моделі.
- Ми використовуємо функцію update для просування стану моделі.
- Ми реагуємо на що надходять в канал actions дії (Action).
- Ми виводимо все це на екран через функцію view.
Замість того, щоб відразу намагатися зрозуміти що ж тут відбувається на кожному рядку, я пропоную спочатку поглянути на схему, що відбувається на високому рівні.
Блакитна частина це наша програма, тобто рівно ті модель / оновлення / Отображаніе, про які ми говорили. Велику частину часу ви можете працювати, не виходячи за межі цього поля.
Новим тут є "канали" і те, як вони дозволяють новим діям (Action) виникати у відповідь на призначений для користувача введення. На зображенні вони зображені пунктирними лініями від монітора до вашій програмі. Коли ми призначаємо певні канали в функції view. ми визначаємо, яким чином дії користувача будуть потрапляти в наш код. Зверніть увагу, що ми не виконуємо ці дії, а просто реєструємо їх для нашої основної програми. Це поділ є ключовою особливістю!
Я хочу зазначити, що цей код з Signal за великим рахунком однаковий для всіх програм в Elm. Ви можете захотіти дізнатися більше про сигнали. але для продовження читання вам буде досить цього загального розуміння. Ми хочемо описати архітектуру, а не загрузнути в тому, як все влаштовано. Тому давайте перейдемо до розширення нашого прикладу!
Приклад №2: Пара лічильників
У першому прикладі ми створили простий лічильник, але як це буде масштабироваться, коли нам знадобиться два лічильника? Чи зможемо ми зберегти модульність?
Основним нашим завданням зараз є повторне використання всього коду попереднього прикладу. Щоб цього домогтися, ми створимо самостійний модуль Counter. в який покладемо всі деталі реалізації лічильника. Єдина зміна буде в функції view. тому я не буду розкривати всі старі визначення.
Тепер, коли у нас є наш базовий модуль Counter. займемося створенням додатка CounterPair. Як завжди, починаємо з моделі:
Наша модель є записом з двома полями, по одному для кожного лічильника, який ми хочемо показати на екрані. Це повністю відображає стан програми. Ще у нас є функція init. створює нову модель коли нам це потрібно.
Далі ми визначаємо набір дій, які ми хочемо підтримувати. Цього разу у нас буде: скидання всіх лічильників, оновлення верхнього лічильника або оновлення нижнього лічильника.
Зверніть увагу, що наш тип-об'єднання вказує на тип Counter.Action. але ми не знаємо подброностей цих дій. Коли ми створюємо функцію update ми просто відправляємо ці дії в правильне місце:
І наостанок залишається тільки зробити функцію відображення, яка покаже на екрані обидва наших лічильника і кнопку скидання.
Приклад №3: Динамічний список лічильників
Пара лічильників це круто, але як щодо списку лічильників, в який можна додавати нові і видаляти на вимогу? Чи буде наш прийом працювати і тут?
У цьому прикладі ми будемо використовувати той же самий модуль Counter. що і в прикладі №2.
Це означає, що ми приступимо відразу до модуля CounterList. Як завжди, починаємо з моделі:
Тепер наша модель це список лічильників, кожен зі своїм унікальним ID. Ці ідентіфікактори дозволяють нам розрізняти лічильники і коли ми захочемо оновити четвертий, у нас буде простий спосіб це зробити. (Крім того ці ID дають зручний спосіб визначати унікальний ключ коли ми задумаємося про оптимізацію відтворення компонентів, але це ми не будемо зараз особливо зупинятися на цій темі). Крім того, у моделі є поле nextId. допомагає нам призначати унікальні ідентифікатори при додаванні нових лічильників.
Наведемо набір дій, які ми можемо застосовувати до нашої моделі. Ми хочемо мати можливість додавати лічильники, видаляти їх, а так само змінювати значення окремих лічильників.
Наш тип-об'єднання Action вельми схожий на описане вище поведінка.
Давайте опишемо функцію update.
Ось загальне опісеніе кожного випадку:
Insert - Спочатку ми створюємо новий лічильник і кладемо його в кінець списку. Потім ми збільшуємо nextID щоб наступного разу у нас вже був готовий ID.
Remove - Видаляємо перший елемент в нашому списку лічильників.
Modify - проходить за списком лічильників і, якщо попався потрібний ID, викликаємо Action для цього лічильника.
Все що залишилося це функція відображення.
Коли ми створюємо функцію view додатки, ми застосовуємо функцію viewCounter на кожен елемент списку. А коли ми створюємо кнопки додавання і видалення, які шлють повідомлення в канал додатки address безпосередньо.
Подібний трюк з ID може бути використаний в будь-якому місці, де вам потрібно динамічне кількість вкладених компонентів. Лічильники це досить просто, але такий патерн буде працювати в точності однаково, якщо у вас буде список профілів користувачів, твітів, елементів стрічки новин або продуктів.
Приклад №4: Високий рівень список лічильників
Окей, тримати речі простими і модульними для списку лічильників це здорово, але що якщо замість загальної кнопки скидання у кожного лічильника буде своя кнопка видалення? Це вже напевно все зламає!
Не, все працює.
В даному випадку нам буде потрібно новий спосіб створювати лічильники разом з їх кнопками. Ми можемо залишити стару функцію view і просто додати нову viewWithRemoveButton. яка буде трохи по-іншому малювати модель. Нам не потрібно дублювати код або робити божевільні трюки з успадкуванням або перевантаженням. Ми просто додамо в публічний інтерфейс модуля нову функцію, що реалізує нову функціональність!
Тепер, коли у нас є viewWithRemoveButton. ми можемо створити модуль CounterList. в якому зберемо всі лічильники разом. Тип Model буде використовуватися такий же, як в прикладі №3: список лічильників і унікальний номер.
Можливі дії будуть трохи відрізнятися. Замість видалення всіх старих счётчтіков ми хочемо видалити тільки той, чий ID співпадає з.
Функція поновлення теж не сильно відрізняється від попереднього прикладу.
Коли приходить Remove ми прибираємо лічильник має ID того, який ми повинні прибрати. В іншому, все приблизно те ж саме.
І нарешті, ми збираємо все це разом в функції view:
основих уроки
Базовий прийом - Все будується навколо типу Model. функції update для її оновлення та функції view для відображення. Далі йдуть тільки варіації цього прийому.
Додавання контексту - Іноді потрібна додаткова інформація для поновлення або відображення моделі. Ми завжди можемо додати в ці функції контекст, не завантажуючи основний тип Model.
На кожному рівні вкладеності ми можемо визначити той Context. який потрібен для внутрішніх компонентів.
Простота тестування - Всі функції є чистими. Це дозволяє їх дуже просто тестувати - не потрібно ніякої особливої ініціаліазціі або штучного оточення, просто передайте їм аргументи, які ви хочете випробувати.
Ще один шаблон
Існує ще один важливий спосіб розширити основний прийом. Наприклад, вам може знадобитися оновити компонент і, в залежності від результату, треба поміняти щось ще в іншій частині програми. Ви можете розширити функцію update. щоб вона повертала більше інформації.
Залежно від логіки обробки update. ми можемо сказати комусь вище оновити вміст або вивести що-небудь. Схожим чином компонент може видалити себе:
Якщо не дуже зрозуміло як це працює, я, може бути, напишу приклад 5, що використовує цей прийом. А поки, ви можете переглянути схожі приклади в забавній версії додатка TodoMVC на Elm.