Як ми розробляємо новий фронтенд Tinkoff.ru

Tinkoff.ru
У квітні цього року ми перезапустили tinkoff.ru. Банк перетворився на фінансовий супермакет. Тепер не тільки клієнт банку, але й будь-який відвідувач сплатить мобільний, перевірить податки і оформить іпотеку — все на одній платформі. У цій статті я поділюся досвідом та технологічними рішеннями, до яких ми прийшли за рік розробки.
Наш стек для фронтенда добре себе показав. Тепер ми вирішуємо проблеми і адаптуємося до нових вимог в рамках концепції архітектури.
За рік ми розробили 350 незалежних інтерфейсних блоків з 3000 React-компонентів. Оптимізували завантаження сторінок, тому що tinkoff.ru відвідують 7 млн користувачів на місяць і число відвідувачів постійно зростає. Фронтенд розробляє 30 чоловік і ми постійно шукаємо талановитих розробників.
Ми об'єднали функції попередніх інтернет-банків, порталу та гаманця. Тому спочатку я опишу вихідні стека, а потім розповім, як ми прийшли до поточного стека.
Огляд попередніх стека
Портал та інтернет-банк 2011
Першу версію ми розробляли внутрішніми ресурсами на основі комерційного рішення від голландської компанії. Бекенд крутився на базі важкого Java/Spring програми. На фронті з-за нестачі гнучкості і докладної документації сформувався стек з jQuery, Backbone, Handlebars.
Maven збирав фронт з беком. Було катастрофічно мало плагінів для фронту, так як Maven не підходив для складання клієнтських пакетів. Це привело нас у глухий кут. Благо знайшли як відокремити клієнтську збірку від серверної за допомогою Grunt.
Використовувати шаблони на сервері і незв'язані шаблони на клієнта зі своєю логікою та архітектурою вважалося нормою. Доводилося підтримувати два UI-шару: серверний UI і клієнтський UI. Коли ми маємо велике RIA – дублюється багато логіки, яка написана на різних мовах програмування. Наприклад: маршрутизація по сторінках, логіка отримання даних або шаблони з однаковою розміткою.
В кінці 2013 року став розвиватися изоморфный підхід до створення веб-додатків. І рішення з дублюванням шаблонів на сервері, і на клієнті стало вважатися концептуально хибним.
Гаманець 2014
Цей проект цікавий з двох причин:
  1. «Тінькофф Мобільний Гаманець» — перший додаток, яке використовували всі, не тільки клієнти банку.
  2. Ми спробували изоморфный підхід на базі архітектури MVC. Відправна точка — стаття від Airbnb.
Стек був схожий з інтернет-банком: Backbone і Handlebars, на сервер Node.js. Частина в'ю рендерилось на сервері. Додаток вирішувала свої завдання і навіть вийшов один UI-шар. Але стало ясно, що на великому додатку подібна архітектура принесе складності. З'явилися проблеми із збагаченням моделей даних в браузері. Доводилося писати окремі контролери для серверної і клієнтської сторони.
Наступний проект розробили за іншою парадигмою.
Інтернет-банк 2015
Інтернет-банк був відділений від зовнішнього сайту і представляв з себе Single Page Application. Для інтернет-банку використовували фреймворк Angular.js. В результаті ми отримали сучасний інтерфейс інтернет-банку.
У 2015 році бізнес змінив стратегію розвитку і пред'явив нові вимоги до веб-додатком. Нам належало:
  • оновити Tinkoff.ru,
  • інтегрувати всі сторінки функціональність інтернет-банку і гаманця,
  • підтримати SEO та SMM для всіх сторінок, включаючи сторінки з платіжними провайдерами.
Новий інтернет-банк мав UI-шар тільки на клієнті. Пошукові роботи поки не навчилися добре індексувати подібні програми. І незрозуміло, коли це станеться. Не вийшло відмовитися від серверного шару. Ми бачили кілька шляхів розвитку існуючих стека:
  1. Об'єднати портал старий і новий інтернет-банк. Портал виступав би як серверного UI-шару, а Angular.js в якості клієнтського. Цей варіант не вирішив би наші фундаментальні проблеми.
  2. Замінити Java додаток Node.js додатком. Це могло б спростити підтримку, але залишалося два UI-шару.
  3. Прибрати Java, залишити тільки Angular.js. І рендери SPA за допомогою розгорнутих серверів з headless браузером Phantom.js. Таку схему складно налагоджувати, тому цей варіант не підходить для великої кількості сторінок.
Ми проаналізували шляхи розвитку існуючих стека і зрозуміли, що вони не вирішують наших завдань. У результаті вибрали кардинально інший підхід.
Платформа 2016
Те, що зараз доступно на Tinkoff.ru ми називаємо платформою.
За основу цієї системи ми вибрали архітектуру Flux, яку запропонували інженери Facebook в 2014 році. Flux відрізняється від MV* тим, що в ньому відсутні різноспрямовані потоки даних, що легше лягає на изоморфную парадигму і все це допомагає швидше налагоджувати додаток. Архітектура Flux взяла за основу модель роботи з базою даних CQRS.
Ми реалізували ідею изоморфного додатки з одним UI-шаром. Як шаблонизатора вибрали бібліотеку React.js з підтримкою віртуального DOM. Завдяки якому шаблони легко рендеряться на сервері.
Склався стек вирішує завдання SEO та SMM. Переиспользует код між браузером і сервером, за винятком специфічного для оточення коду, наприклад робота з cookies. Дозволяє вирішувати весь спектр завдань однією групою розробників, що призводить до прискорення роботи. Не залежить від одного фреймворку/вендора. Додаток об'єднується набором правил, вибудовується з невеликих незалежних модулів. Модуль можна замінити, якщо буде відповідна реалізація.
Універсальне додаток
Fluxible
В якості реалізації Flux вибрали фреймворк Fluxible від інженерів Yahoo. Ця реалізація орієнтована на изоморфный рендер. Вибране рішення повністю задовольняє нашим вимогам.
Ми намагаємося не пов'язувати додаток великими залежностями, тому використовуємо тільки дві бібліотеки з набору:
  1. Routr – маршрутизація по сторінкам. Бібліотека підтримує express-подібні шляху. Вона изоморфная і швидка: зараз роутимся по 2000 сторінок.
  2. Dispatchr – Flux диспетчер.
Розподіл по верствам
Архітектура програми
image
Сервіси. Доступ до браузерних або HTTP API, взаємодію із зовнішніми системами. Тут міститься частина бізнес-логіки. Сервіси мають декілька реалізацій: ізоморфні shared server node.js і client для браузера. Можуть кешувати результат.
Дії. Flux action creators, містять частину UI і бізнес логіки. Мають доступ до сервісів.
Стори. Моделі даних, які містять UI логіку.
Компоненти. Рендер даних з сторів в HTML.
Прогресивна завантаження
Завдяки серверному рендеру ми отримуємо ефект прогресивної завантаження і скорочення time to glass. Середній користувач бачить працюючу сторінку через 600 мс після запиту сайту. Через пару секунд ініціалізується динаміка і завантажуються персональні дані.
image
Ми можемо рендери всі дані на сервері, можемо ренедрить частина. А можемо повністю відключити серверну частину та використовувати програму як SPA. Щоб зменшити навантаження на сервер, рендерим лише загальні відомості для всіх користувачів, а роботу з персональними даними виконуємо у браузері.
image
Приклад коду
Що виконувати на сервері або що дозволити користувачеві запустити з певними ролями, ми визначаємо на рівні функції створення дії:
import { ACCOUNT_LIST } from '../actions';
import { CLIENT } from '../roles';

accountList.isServer = true; // Прапор указує на право запуску дії на сервері
accountList.requiredRoles = [CLIENT]; // Список вказує необхідні ролі користувача

function accountList(context) {
return context.service('account/list') // Кожна частина програми має свій обмежений контекст з набором методів із загального контексту. Таким чином викликаємо сервіси.
.then(payload => context.dispatch(ACCOUNT_LIST, payload)); // Далі диспатчим дію з корисним навантаженням
}

export default accountList;

В іншому коді програми немає умов для визначення оточення. Все вирішується на рівні контексту, який успадковується від базового класу контексту.
Переиспользование
Компоненти доводилося використовувати з різними моделями даних. Їх стало складно підтримувати, перевіряти і переиспользовать. Тому ми розділили компоненти на коннектори і чисті компоненти:
image
Такий підхід спростив переиспользование компонентів і тестування.
Приклад коннектора
import { LogoPure } from './LogoPure.jsx';

const UILogo = connect(
['config', 'brands'],
({
brands,
config: { brandsBasePath }
}) => ({
brands,
brandsBasePath
})
)(LogoPure);

export default UILogo;

Використаємо утиліту connect (схожа з коннектором з redux), яку можна використовувати у вигляді декоратора. Перший аргумент – список сторів, в яких ми зацікавлені. Другий аргумент – функція мапінгу, яка приймає стан всіх сторів і повертає тільки потрібні чистого компоненту дані.
Higher-order Components
Наступний підхід, який ми спочатку не використовували для наслідування коду — компоненти високого порядку. Переиспользовать код між компонентами допомагали міксини, які підтримує React.createClass.
Міксини не гарантують своє оточення. Якщо на компоненті використовувати більше трьох миксинов і якщо вони стають залежні, то підтримка такого компонента стає проблемою. Детальніше про це у статті Mixins Are Dead. Long Live Composition.
Оптимізація
Оптимізація універсального додатка знаходиться на двох сторонах: клієнтської і серверної. Розповім про наші підходи до розробки швидкого програми.
Оптимізація клієнта
Перше правило швидких компонентів – як можна рідше викликати метод render. Для цього ми використовуємо підхід pure-render. Він викличе render через перевизначення методу shouldComponentUpdate, якщо тільки вихідні дані змінилися (props або state).
Іноді компонент більше даних, ніж йому потрібно. Іноді змінюються поля моделі, дані не змінюються, а посилання на модель даних змінилася. В цьому випадку перевірка pure-render не спрацьовує. Щоб визначити кількість викликів render того чи іншого компонента, ми використовуємо модуль react-perf. З його допомогою отримуємо статистику в зручному вигляді:
image
Якщо знаходимо компонент з невиправдано великою кількістю ререндеров, то діагностуємо його детальніше з допомогою нашої утиліти render-logger. Вона дозволяє побачити змінені дані, які спричинили дзвінок render:
image
Рендер стався через зміни функції, переданої у властивість onClick. Це відбувається при використанні bind або визначенні функції всередині render батьківського компонента, коли при кожному виклику render створюється нова функція. І з-за цього не спрацьовує захист pure-render дочірнього компонента.
Щоб заборонити bind в render, використовуємо линтинг Eslint з плагіном eslint-plugin-react і опцією jsx-no-bind.
Batched updates
React підтримує зміну стратегії фонового в runtime. Ми використовуємо стратегію пакетного оновлення при початковій ініціалізації. Або при переходах між сторінками, коли відбувається підвищена активність програми. Це зменшує загальний час рендера.
Візуальне прискорення
Наступний спосіб, який ми використовуємо для прискорення клієнтського примірника програми, це візуальне прискорення. Вважаю цей спосіб кращим за співвідношенням витрат до результату.
Зараз на головній Tinkoff.ru є шапка з логотипами платіжних провайдерів, які з'являються з анімацією.
imageimage
У перших версіях логотипи з'являлися тільки після завантаження клієнтського пакета JS. На повільному мобільному інтернеті це призводило до неприємного ефекту довгої завантаження сторінки. Користувач чекав, коли сірий блок буде наповнений.
Ми вирішили змінити стилі та код компонента, щоб перетворювати логотипи на сервері. Завдяки відмові від очікування ініціалізації клієнтської частини, зменшився час до появи логотипів.
Оптимізація на сервері
Серверний рендер компонентів React повільніше простих строкових шаблонизаторов. Я розповім про те, як ми прискорюємо рендер на сервері.
  1. Використовуємо ES6 Class, а не React.createClass (він повільніше із-за autobinding) і не забуваємо NODE_ENV=production, який відключає код для налагодження. Це призводить до 4х кратним прискорення.
  2. Минифицированная версія React. Код для профайлінгу видалений повністю. +2% (тут і далі приріст відносно першого пункту)
  3. Трансформація коду на рівні Babel. Використовуємо два плагіна:
    1. transform-react-constant-elements піднімає ініціалізацію компонентів у верхній скоуп модуля
    2. transform-react-inline-elements перетворює виклик createElement до оптимізованому вигляді. +10%

  4. Максимально використовуємо stateless функції. На тестовому стенді заміна всіх компонентів привела до збільшення в 45%.
  5. React-dom-stream приводить результат функції renderToString до потоку, що дає зменшення часу до віддачі першого байта (TTFB) на нашому стенді до 3-5мс. І до скорочення часу до передачі останнього байта (TTLB) на 55%.
    На даний момент бібліотека не підтримує React 15. Можливо, функціональність бібліотеки включать в ядро React: https://github.com/aickin/react-dom-stream/issues/18 Крім цього, бібліотека реалізує цікаву можливість кешування компонентів окремо, що може прискорити серверний рендер.
На даний момент ми кешуємо всю сторінку. Завдяки відсутності персональних даних в HTML, нам це далося легко.
Кеш
Кешування в додатку реалізовано на декількох рівнях:
  1. Nginx кешує на короткий проміжок часу результат генерації всієї сторінки.
  2. Express-cache-on-deamand страховка від потоку однотипних запитів.
  3. Lru-cache бібліотеки використовуємо для кешування результатів сервісів.
З lru-cache ми використовуємо нестандартну схему видалення даних з кеша. Будь-які дані при досягненні TTL не видаляються з пам'яті, а запитуються у джерела. Якщо джерело відповів новим результатом – кладемо значення в кеш. Такий підхід підвищує відмовостійкість, коли зовнішні системи недоступні. Якщо ми один раз отримали дані з зовнішнього сервісу, то вже не втратимо.
бібліотека, яка реалізує схожу схему:
image
Збірка і деплой
Для складання і деплоя використовуємо CI Teamcity. Робимо два окремих артефакту для клієнта і сервера. На збірці клієнтського пакета звичайна конфігурація Webpack. Зі складанням серверного пакету цікавіше.
Спочатку ми просто збирали пакет tar з вихідних і розпаковували його на сервері. Коли модулів для компіляції Babel стало багато, перші запити на сервер стали проходити довго – Babel компилировал все при перших запитах.
Перше реалізоване рішення – процес прогріву перед стартом, компіляція.
Потім налаштували прекомпиляцию Babel при складанні артефакту, що скоротило час прогріву до пари секунд.
Зараз в момент прогрівання відбувається тільки наповнення кешей даними із зовнішніх сервісів. Такий підхід заощаджує час на складання з допомогою перевикористання часу, витраченого на компіляцію Babel клієнтської збірці.
Друге рішення, яке можна використовувати – компіляція серверного пакету за допомогою Webpack. Цей підхід більш функціональний, але спричиняє збільшення загального часу складання.
У артефакти ми не записуємо змінні оточення, а передаємо їх тільки при старті примірники серверного додатка. Значення змінних оточення записуються в стор і передаються на клієнт через стандартний процес передачі стану програми з сервера на клієнт.
Для запуску і моніторингу програми на сервері використовуємо application runner PM2. PM2 має багату функціональність.
  1. Для запуску програми використовуємо команду pm2 startOrGracefulReload processes.json, що дозволяє перезапускати програму з мінімальним часом недоступності.
    processes.json:
    {
    "name": "portal",
    "script": "server/index.js",
    "instances": -1
    }
  2. Якщо ваш додаток запускається більш ніж на одному сервері, то між ними є балансування. І в цьому випадку балансування між форками з допомогою Node.js кластера стає непотрібною. Ми прибрали балансування балансування і переклали цю відповідальність на єдину точку – Nginx. Кожен екземпляр програми на всіх серверах запускається на окремому порту і Nginx знає про всіх примірниках. Завдяки PM2 зробити це просто. При виборі порту потрібно враховувати змінну оточення NODE_APP_INSTANCE: PORT: process.env.PORT + process.env.NODE_APP_INSTANCE
image
Завершення
Універсальне веб-додаток вирішує виставляються завдання, об'єднує одну відповідальність в одну кодову базу. React.js змусив переосмислити усталені практики розробки веб-інтерфейсів. Віртуальний DOM стає стандартом де-факто для шаблонизаторов. Flux спрощує розробку і налагодження складних додатків. Node.js продовжує виборювати своє законне місце на корпоративних серверах.
Якщо ви не розробляли програми на схожому стеці, то, сподіваюся, прочитання статті надихнуть вас на сміливі експерименти в цьому напрямку. Думаю, в найближчі декілька років не з'явиться кардинально нових підходів та інструментів для розробки додатків, які б спрощували порядок розроблення. Є сенс вкласти трохи свого часу в вивчення React.js, Flux і приєднатися до нашої Dream Team.
Пишіть про себе на best-talents@tinkoff.ru.
Ділюся фотографіями з одного планування та звичайної п'ятничної тусовки:
image
image
Дякуємо і до нових зустрічей!
PS: Якщо ви хочете отримати навик промислової розробки веб-додатків, а також навички створення кінцевих продуктів, то ласкаво просимо в нашу літню школу FinTech. Курси ведуть наші фахівці і за підсумком ви навчитеся створювати фінансові продукти.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.