Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваны

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваны

Мне заўсёды было цікава, як уладкованы Хабр знутры, як пабудаваны workflow, як выбудаваны камунікацыі, якія прымяняюцца стандарты і як тут наогул пішуць код. На шчасце, такая магчымасць у мяне з'явілася, бо нядаўна я стаў часткай хабракаманды. На прыкладзе невялікага рэфактарынгу мабільнай версіі паспрабую адказаць на пытанне: якое гэта - працаваць тут фронтам. У праграме: Node, Vue, Vuex і SSR пад соусам з нататак аб асабістым вопыце ў Хабры.

Першае, што трэба ведаць аб камандзе распрацоўкі - нас мала. Мала — гэта тры фронты, два бэкі і тэхлід усяе Хабра — Бакслі. Ёсць, вядома, яшчэ тэстыравальнік, дызайнер, тры Вадзіма, цуд-венік, маркетолагіня і іншыя Бумбурумы. Але непасрэдных кантрыб'ютараў у сорцы Хабра ўсяго шэсць. Такое сустракаецца даволі рэдка - праект са шматмільённай аўдыторыяй, які звонку выглядае як гіганцкі энтэрпрайз, на справе больш падобны на ўтульны стартап з максімальна плоскай арганізацыйнай структурай.

Як і многія іншыя IT-кампаніі, Хабр вызнае ідэі Agile, практыку CI і вось гэта вось усё. Але па маіх адчуваннях, Хабр як прадукт развіваецца хутчэй хвалепадобна, чым бесперапынна. Так некалькі спрынтаў запар мы старанна нешта кадзім, праектуем і перапраектуем, ламаем нешта і робім, рэзалвім цікеты і заводзім новыя, наступаем на граблі і страляем сабе ў ногі, каб нарэшце рэлізнуць фічу ў прад. А затым надыходзіць некаторы зацішша, перыяд перапланіроўкі, час рабіць тое, што знаходзіцца ў квадранце "важна-нетэрмінова".

Як раз пра такі «міжсезонны» спрынт і пойдзе гаворка ніжэй. На гэты раз у яго патрапіў рэфактарынг мабільнай версіі Хабра. На яе наогул у кампаніі ўскладаюць вялікія надзеі, і ў даляглядзе яна павінна замяніць сабой увесь заапарк інкарнацый Хабра і стаць універсальным кросплатформавым рашэннем. Калі-небудзь тут з'явіцца і адаптыўная вёрстка, і PWA, і афлайн-рэжым, і карыстацкая кастамізацыя, і шмат чаго цікавага.

Ставім задачу

Неяк раз на радавым стэндапе, адзін з франтоў распавёў аб праблемах у архітэктуры кампанента каментароў мабільнай версіі. З гэтай падачы мы арганізавалі мікра-нараду ў фармаце групавой псіхатэрапіі. Кожны па чарзе казаў, дзе ў яго баліць, усё фіксавалі на паперы, спачувалі, разумелі, хіба што ніхто не пляскаў. На выхадзе атрымаўся спіс з 20 праблем, які ясна даваў зразумець, што мабільны Хабр павінен прарабіць яшчэ доўгі і цярністы шлях да поспеху.

Мяне турбавала найперш эфектыўнасць выкарыстання рэсурсаў і тое, што называецца smooth interface. Кожны дзень на маршруце «хата-праца-хата» я бачыў як мой старэнькі тэлефон адчайна спрабуе адлюстраваць 20 загалоўкаў у стужцы. Выглядала гэта прыкладна так:

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваныІнтэрфейс мабільнага Хабра да рэфактарынгу

Што тут адбываецца? Калі коратка, то сервер аддаваў HTML старонку ўсім аднолькава, па-за залежнасцю залогінен карыстач ці не. Затым загружаецца кліенцкі JS і зноўку запытвае неабходныя дадзеныя, але ўжо з папраўкай на аўтарызацыю. То бок, фактычна мы рабілі адну і тую ж працу двойчы. Інтэрфейс мігацеў, а карыстач загружаў добрую сотню лішніх кілабайт. У падрабязнасцях усё выглядала яшчэ больш жудасна.

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваныСтарая схема SSR-CSR. Аўтарызацыя магчымая толькі на этапах С3 і С4, калі Node JS не заняты генераваннем HTML і можа праксіраваць запыты на API.

Нашую архітэктуру таго часу вельмі дакладна апісаў адзін з карыстальнікаў Хабра:

Мабільная версія дзярмо. Кажу як ёсць. Жудаснае спалучэнне SSR разам з CSR.

Мы змушаныя былі гэта прызнаць, як бы сумна гэта ні было.

Я прыкінуў варыянты, паставіў сабе тыкет у «Джыры» з апісаннем на ўзроўні «цяпер дрэнна, зрабі нормаў» і шырокім мазкамі дэкампазіраваў задачу:

  • перавыкарыстоўваць дадзеныя,
  • мінімізаваць колькасць перамалёвак,
  • выключыць дублі запытаў,
  • зрабіць працэс загрузкі больш відавочным.

Перавыкарыстоўваем дадзеныя

У тэорыі server-side rendering закліканы вырашыць дзве задачы: не пакутаваць ад абмежаванняў пошукавых сістэм па частцы індэксавання SPA і палепшыць метрыку FMP (непазбежна пагоршыўшы TTI). У класічным сцэнары, які канчаткова сфармулявалі ў Airbnb у 2013 годзе (яшчэ на Backbone.js), SSR – гэта тое ж самае ізаморфнае JS-дадатак, запушчанае ў асяроддзі Node. Сервер проста аддае ў якасці адказу на запыт згенераваную вёрстку. Затым адбываецца рэгідрацыя на баку кліента, і далей усё працуе без перазагрузак старонкі. Для Хабра, як і для многіх іншых рэсурсаў з тэкставым напаўненнем, серверны рэндэрынг - крытычна важны элемент пабудовы дружалюбных адносін з пошукавікамі.

Нягледзячы на ​​тое, што з моманту з'яўлення тэхналогіі прайшло ўжо больш за шэсць гадоў, і за гэты час у свеце фронтэнда выцекла сапраўды шмат вады, для многіх распрацоўшчыкаў гэтая ідэя ўсё яшчэ пакрыта заслонай таямніцы. Мы не засталіся ў баку, і выкацілі ў прад Vue-дадатак з падтрымкай SSR, выпусціўшы адну маленькую дэталь: мы не пракінулі initial state на кліент.

Чаму? Дакладнага адказу на гэтае пытанне няма. Ці то не хацелі павялічваць памер адказу ад сервера, ці то з-за букета іншых архітэктурных праблем, ці то проста не ўзляцела. Так ці інакш пракінуць state і перавыкарыстоўваць усё, што рабіў сервер, здаецца суцэль мэтазгоднай і карыснай справай. Задача насамрэч трывіяльная state проста инжектится у кантэкст выканання, і Vue аўтаматычна дадае яго да згенераванай вёрсткі ў якасці глабальнай зменнай: window.__INITIAL_STATE__.

Адна з узніклых праблем - немагчымасць пераўтварыць у JSON цыклічныя структуры (кругавая спасылка); вырашалася простай заменай такіх структур на іх плоскія аналагі.

Акрамя таго, маючы справу з UGC-кантэнтам варта памятаць, што дадзеныя варта пераўтвараць у HTML-entities, для таго, каб не зламаць HTML. Для гэтых мэт мы выкарыстоўваем he.

Мінімізуем перамалёўкі

Як відаць са схемы вышэй, у нашым выпадку адзін інстанс Node JS выконвае дзве функцыі: SSR і "проксі" у API, дзе як раз адбываецца аўтарызацыя карыстача. Гэтая акалічнасць робіць немагчымым аўтарызацыю ў момант выканання JS-кода на серверы, бо нода аднаструменная, а функцыя SSR сінхронная. Гэта значыць сервер проста не можа адпраўляць запыты сам на сябе, пакуль коллстэк чымсьці заняты. Атрымалася так, што state мы пракінулі, але інтэрфейс не пераставаў тузацца, бо дадзеныя на кліенце вынікала абнавіць з улікам карыстацкай сесіі. Трэба было навучыць наша дадатак класці ў initial state правільныя дадзеныя з улікам лагіна карыстальніка.

Рашэнняў праблемы знайшлося ўсяго два:

  • чапляць аўтарызацыйныя дадзеныя да міжсерверных запытаў;
  • разбіць пласты Node JS у два асобных інстанса.

Першае рашэнне патрабавала выкарыстаць глабальныя зменныя на серверы, а другое расцягвала тэрміны рэалізацыі задачы прынамсі на месяц.

Як зрабіць выбар? Хабр часта рухаецца па шляху найменшага супраціву. Нефармальна тут існуе нейкае агульнае імкненне скарачаць да мінімуму цыкл ад ідэі да прататыпа. Мадэль стаўлення да прадукта чымсьці нагадвае пастулаты booking.com, з той толькі розніцай, што Хабр куды больш сур'ёзна ставіцца да карыстацкага фідбэка і давярае прыняцце падобных рашэнняў табе як распрацоўніку.

Прытрымліваючыся гэтай логіцы і свайму ўласнаму жаданню барзджэй вырашыць праблему, я абраў глабальныя зменныя. І, як гэта часта здараецца, за іх рана ці позна даводзіцца плаціць. Мы заплацілі амаль адразу: папрацавалі ў выходныя, разграблі наступствы, напісалі пасмяротнае і пачалі дзяліць сервер на дзве часткі. Памылка была вельмі дурной, а баг з яе ўдзелам прайграваўся няпроста. І так, за такое сорамна, але так ці інакш, спатыкаючыся і крэкчучы, мой PoC з глабальнымі зменнымі ўсё ж выйшаў у прадакшн і суцэль паспяхова працуе ў чаканні пераезду на новую «двухнодную» архітэктуру. Гэта быў важны крок, бо фармальна мэта была дасягнута – SSR навучыўся аддаваць цалкам гатовую да выкарыстання старонку, а UI стаў нашмат больш спакойным.

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваныІнтэрфейс мабільнага Хабра пасля першага этапу рэфактарынгу

У канчатковым рахунку архітэктура SSR-CSR мабільнай версіі вядзе вось да такой карціны:

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваныДвухнодная схема SSR-CSR. Node JS API заўсёды гатова да асінхроннага I/O і не блакуецца функцыяй SSR, бо апошняя знаходзіцца ў асобным інстансе. Ланцужок запытаў #3 не патрэбны.

Выключаем дублі запытаў

Пасля праведзеных маніпуляцый, першапачатковы рэндэр старонкі перастаў правакаваць эпілепсію. Але далейшае выкарыстанне Хабра ў рэжыме SPA усё яшчэ выклікала здзіўленне.

Бо аснову user flow складаюць пераходы выгляду. спіс артыкулаў → артыкул → каментары і назад, важна было аптымізаваць выдатак рэсурсаў гэтага ланцужка ў першую чаргу.

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваныЗварот да стужкі пастоў правакуе новы запыт дадзеных

Глыбока капаць не прыйшлося. На скрынкасце вышэй відаць, што прыкладанне перазапытвае спіс артыкулаў пры свайпе назад, прычым падчас запыту мы артыкулы не бачым, значыць папярэднія дадзеныя кудысьці знікаюць. Выглядае ўсё так, быццам кампанент спісу артыкулаў выкарыстоўвае лакальны стейт і губляе яго на destroy. На самай жа справе, прыкладанне выкарыстоўвала глабальны стейт, але архітэктура Vuex была пабудавана у ілоб : модулі прывязаныя да старонак, якія ў сваю чаргу прывязаныя да роўтаў. Прычым усе модулі "аднаразовыя" - кожны наступны заход на старонку перапісваў модуль цалкам:

ArticlesList: [
  { Article1 },
  ...
],
PageArticle: { ArticleFull1 },

Разам у нас быў модуль ArticlesList, які змяшчае ў сабе аб'екты тыпу Артыкул і модуль PageArticle, які з'яўляўся пашыранай версіяй аб'екта Артыкул, свайго роду ArticleFull. Па вялікім рахунку, дадзеная рэалізацыя нічога жудаснага ў сабе не нясе - гэта вельмі проста, можна нават сказаць наіўна, але лімітава зразумела. Калі выпілаваць абнуленне модуля пры кожнай змене роута, то з гэтым можна нават жыць. Аднак пераход паміж стужкамі артыкулаў, напрыклад /feed → /all, гарантавана выкіне ўсё, што звязана з персанальнай стужкай, бо ў нас усяго адзін ArticlesList, у які трэба пакласці новыя дадзеныя. Гэта зноў нас прыводзіць да дублявання запытаў.

Сабраўшы ў кучу ўсё, што ўдалося раскапаць па тэме, я сфармуляваў новую структуру стейта і прадставіў яе калегам. Абмеркаванні былі працяглымі, але ў выніку аргументы "за" пераважылі сумневы, і я прыступіў да рэалізацыі.

Логіка рашэння лепш за ўсё раскрываецца за два этапы. Спачатку мы спрабуем адвязаць модуль Vuex ад старонак і прывязаць напрамую да роўт. Так, дадзеных у старозе стане крыху больш, гетэры стануць крыху складаней, але мы не будзем грузіць артыкулы па два разы. Для мабільнай версіі, гэта, мабыць, наймацнейшы аргумент. Атрымаецца прыкладна так:

ArticlesList: {
  ROUTE_FEED: [ 
    { Article1 },
    ...
  ],
  ROUTE_ALL: [ 
    { Article2 },
    ...
  ],
}

Але што калі спісы артыкулаў могуць перасякацца паміж некалькімі роўтамі і што, калі мы хочам перавыкарыстоўваць дадзеныя аб'екта Артыкул для адмалёўкі старонкі посту, ператварыўшы яго ў ArticleFull? У гэтым выпадку, больш лагічным было б выкарыстанне такой структуры:

ArticlesIds: {
  ROUTE_FEED: [ '1', ... ],
  ROUTE_ALL: [ '1', '2', ... ],
},
ArticlesList: {
  '1': { Article1 }, 
  '2': { Article2 },
  ...
}

ArticlesList тут - гэта проста нейкае сховішча артыкулаў. Усіх артыкулаў, якія былі загружаны на працягу карыстацкай сесіі. Мы ставімся да іх максімальна беражліва, бо гэта трафік, які, магчыма, быў загружаны праз боль дзе-небудзь у метро паміж станцыямі, і мы зусім дакладна не жадаем прычыніць карыстачу гэты боль зноў, прымусіўшы яго грузіць дадзеныя, якія ён ужо загрузіў. Аб'ект ArticlesIds з'яўляецца проста масівам айдышнікаў (як бы «спасылак») на аб'екты Артыкул. Такая структура дазваляе не дубляваць агульныя для роўтаў дадзеныя і перавыкарыстоўваць аб'ект. Артыкул пры рэндэры старонкі паста з дапамогай мержа ў яго пашыраных дадзеных.

Выснова спісу артыкулаў стаў таксама больш празрыстым: кампанент-ітэратар перабірае масіў з айдышнікамі артыкулаў і адмалёўвае кампанент цізер артыкула, перадаючы Id у якасці пропса, а даччыны кампанент у сваю чаргу дастае патрэбныя дадзеныя з ArticlesList. Пры пераходзе на старонку публікацыі, мы дастаем ужо наяўную дату з ArticlesList, які робіцца запыт на атрыманне якія адсутнічаюць дадзеных і проста дадаем іх да існага аб'екта.

Чаму гэты падыход лепшы? Як я пісаў вышэй, такі падыход беражлівей у адносінах да загружаных дадзеных і дазваляе перавыкарыстоўваць іх. Але апроч гэтага ён адчыняе дарогу некаторым новым магчымасцям, якія выдатна ўпісваюцца ў такую ​​архітэктуру. Напрыклад, полінг і падгрузка артыкулаў у стужку па меры іх з'яўлення. Мы можам проста скласці свежыя пасты ў «сховішча» ArticlesList, захаваць асобны спіс новых айдышнікаў у ArticlesIds і паведаміць карыстальніка пра гэта. Пры націску на кнопку "Паказаць новыя публікацыі", мы проста ўставім новыя Id у пачатак масіва бягучага спісу артыкулаў і ўсё будзе працаваць амаль магічным чынам.

Які робіцца загрузку прыемней

Вішанькай на торце рэфактарынгу стала канцэпцыя шкілетонаў, якая робіць працэс загрузкі кантэнту на павольным інтэрнэце крыху менш агідным. Ніякіх дыскусій на гэты конт не было, шлях ад ідэі да прататыпа заняў літаральна дзве гадзіны. Дызайн намаляваўся практычна сам, і мы навучылі нашы кампаненты рэндэрыць немудрагелістыя, ледзь-мігатлівыя div-блокі падчас чакання дадзеных. Суб'ектыўна такі падыход да лоадынг сапраўды памяншае колькасць гармонаў стрэсу ў арганізме карыстальніка. Скелетон выглядае так:

Логі фронтэнд-распрацоўшчыка Хабра: рэфактарымы і рэфлексаваны
Хабралаадынг

Рэфлексаваны

Я паўгода працую ў Хабры і знаёмыя па-ранейшаму пытаюцца: ну што, як табе там? Добра, камфортна - так. Але ёсць сёе-тое, што адрознівае гэтую працу ад іншых. Я працаваў у камандах, якія былі абсалютна абыякавыя да свайго прадукта, не ведалі і не разумелі, хто іх карыстачы. А тут усё па-іншаму. Тут адчуваеш адказнасць за тое, што робіш. У працэсе распрацоўкі фічы, ты часткова становішся яе оўнерам, прымаеш удзел ва ўсіх прадуктовых сустрэчах, звязаных з тваім функцыяналам, уносіш прапановы і сам прымаеш рашэнні. Рабіць прадукт, якім штодня карыстаешся сам, вельмі крута, а пісаць код для людзей, якія, магчыма, разбіраюцца ў гэтым лепш за цябе - проста неверагоднае адчуванне (no sarcasm).

Пасля рэлізу ўсіх гэтых зменаў мы атрымалі пазітыўны фідбэк, і гэта было вельмі і вельмі прыемна. Гэта натхняе. Дзякуй! Пішыце яшчэ.

Нагадаю, што пасля глабальных зменных мы вырашыліся на змену архітэктуры і вылучэнне проксіруючага пласта ў асобны інстанс. "Двухнодная" архітэктура ўжо дабралася да рэлізу ў выглядзе публічнага бэта-тэставанні. Цяпер любы жадаючы можа пераключыцца на яе і дапамагчы нам зрабіць мабільны Хабр лепш. На сёння ўсё. З задавальненнем адкажу на ўсе вашыя пытанні ў каментарах.

Крыніца: habr.com

Дадаць каментар