Оптымістычны інтерфейс без несувязанасцяў: кадры стану, адкаты і абраныя запиты
Выучыце, як ствараць оптымістычныя апдэйты, якія застаюцца правильнымі: фіксаванне стану, мгновенная апдэйтавання, вярнэнне да пачатковага стану у разе абяронкі, анульванне застарэлых запыткаў, а таксама ведаць, калі не трэба выкорыстоўваць гэты патэрн.
Працюючы з швайным з’ўязкам, процес адправы і прыемы сігналу пасля нажатня кнопкі «Падаражыць» застаецца непазначальным. Але калі прыстрой находзится між слабкім LTE і практычна непрацездатным 3G, той жа процес можа зайняць пяць-десять секунд, і за гэты час корыстнік не ведае, чы рэальна была адпаведная дзія. Оптымістычныя падходы закрываюць гэты прыемак, спачатку змінюючы інтерфейс, а толькі пасля — запрашваючы дадзеныя у сервера. У гэтым кераванні показана, як реалізаваць гэты падход у трох чыстых кроках, як захістіць його ад занадто швайных запитоў, і як выбраць, дзе ён будзе корыстны, а дзе — стане прычынай проблем.
Проблэма, якая хаваецца за адним нажатнем
Уявіце параграф, які мае 999 лайкаў і абрамкованае серцэ. У найстрогей реалізацыі, падпісв на серцэ адправляе запит да сервера, які можа знаходзіцца на іншым контыненте. Сервер автентыфікуе абэранта, запісвае інформацыю ў базу дадзеных і адпавядае, і толькі тады серцэ стае чырвоным, а лічылка перайходзіць на 1 000. Такое чаканне ў межах кальколькох сотых мілісекунд ўсё ж прыемнае, але у п’яці секундах стае некантовым. Людзі, які не бачаць адпаведзі, падпісваюць знову, потым ўтрое, і востатку заўсёды здаюцца з аплікэйшнам, таму такая затрымка є выключным спосабам пазбавіцца корыстувачаў.
Падпісв таксама выклікае больш работы, чым простая змяна значэння булевага параметра. Тыповая платформа выконвае тры дзеяння на бэкендзе:
- Яна запоўнюе канцэнтр API, які падвышае лічбу лайкаў для таго параграфа.
- Яна дадае параграф у зберажаную колекцыю на адзеўце корыстувача, ўпэўніваючыся, што яго можна будзе знайсці пазней.
Усе тры элементы залежаць ад сеті. Аднак у фронтэнде патрабуецца змяніць толькі два станы: чы рэзалец заполнены, і цифру пад якім. Якщо інтэрфейс чакае кожныя эфекты з боку сервера пры зміне будзь-каго з цых значэнняў, дапыт вярціцца несправным, нават калі ён працуе правільна.
Оптымізм як намеровая, восстанавліваемая брехня
Оптымістычная аднавленне рашае гэтыя проблемы, калі фронтэнд негайна змянюе свой сопны стан, вырашаючы, што запрошэнне, якое ён ўжо адправіў, будзе успехам. Як толькі пальцы адыходзяць ад экрана, рэзалец становіцца чырвоным, а лічба паказвае 1 000, хоць нічога ўжо не дасягла сервера. Гэтая маленькая неправда і ўчыняе взаімадзейнасць адчуваемай як мгновенная.
Гэта ўзяцца за прыемнае толькі тады, калі існуе план на тое, што будзе, якщо заўважэння не спрацюе. Паезд можа зайсці ў тунель і абарвіць з’ѐеднанне, або сервер можа вернуць памылку. Якщо залишыць показчык на роўні 1,000, хоча база дадзеных яшчэ паказвае 999, стварыцца несувязанасць: інтэрфейс і адпаведны джераг дахоўкі не згодны. Тады фронтэнд должен вернуцца да рэальнага стану — з адпаведным символам і значэнням 999 — і зазвычай паведаміць корыстніка за дапамою карточкі паведамлення, короткага тэксту, який на момент з’яўляецца і зараз жа знікае, напрыклад: "Не удалося захаваць. Прабуйце знову."
Адкліканне гэтага патэрану ў трох крокаў
Змена значэння зменной і надзея на тое, што запит спрацюе, — гэта проста здагадка, а не частка дизайну. Надзеяна, надзейная апдэйтаванне раздзеляе тры задачы: зафіксаваць текущы стан, паказаць адпаведны рэзультат, а потым супрацаваць з адказам сервера.
Крок 1: зафіксаваць моментальную копію
Перш чым ўсё на экране змяніцца, запісайце значэнняя, якія корыстувач можа зараз бачыць — чы рэпост падабраны і які ў нём зараз лічылак. Функцыя-обрабоўкавальнік прыме рэпост, функцыю для змены стану і функцыю падтверджэння, а таксама імпортуе API-званне, якое ён у канцы канцоў здзейсніць:
// likeButton.js
import { updateLikeOnServer } from "./api.js";
async function handleLikeClick(post, setPost, showToast) {
const previousState = { liked: post.liked, likeCount: post.likeCount };
// act two goes here
// act three goes here
}
Пасля гэтага крока у вас будзе адны незменены об’ект, які точна описвае тое, што ўсё на экране, і які гатовы будзе восстановленаць, якщо ўтворыцца проблема. Важна скопіюваць гэтыя два поля у новы об’ект: якщо вы захавайце кансэльтацыю да зменяемага об’екта, які пазней зменяецца, то „снэпшот“ таксама зменіцца разам з яным.
Крок 2: застосаваць бажаны рэзультат
Далей безпасова аднавіце інтерфейс прямо, яшчэ перш чым пачнёцца сеанс з’ўязку:
async function handleLikeClick(post, setPost, showToast) {
const previousState = { liked: post.liked, likeCount: post.likeCount };
setPost({ liked: true, likeCount: post.likeCount + 1 });
// act three goes here
}
На экране тепер выклікаецца тое, чаго прагнаў корыстнік – адзін чырвоны серца і большая колькасць, тады як сервер яшчэ нічога не ведае. Адна деталь, на якую трэба зважыць у вашам кодзе: у гэтым прыкладзе перадаецца абсалютна новы об’ект, які мае толькі liked і likeCount. Якщо ваш метод змены заменяе весь пост усім новым значэнням заместа з’едынаць поля, неабходна распрастарыць існуючы пост у новае значэння, каб такія атрыбуты, як id, не былі втрачаны.
Шаг 3: супранормалізацыя з серверам
На завершанне трэба адправіць рэальны запит і альбо застаўіць оптымістичны стан, альбо яго знішчыць. Запит знаходзіцца ў блоке try; якщо ён выклікае адхыленне, працоўнік восстанавляе поперадні стан і паказвае спавешчэнне:
async function handleLikeClick(post, setPost, showToast) {
const previousState = { liked: post.liked, likeCount: post.likeCount };
setPost({ liked: true, likeCount: post.likeCount + 1 });
try {
await updateLikeOnServer(post.id);
} catch (error) {
setPost(previousState);
showToast("Couldn't save that. Try again.");
}
}
Дапаможнік API выкарыстоўвае fetch браузера. Заўважкай, што fetch адхоўляе запыт толькі у разе проблем з сетчу, а не па статусам HTTP-памылак, таму дапаможнік пераглядае response.ok і явна выклікае памылку. Без гэтага перагляду адпаведны статус 500 будзе спрыяць вважацца успехам, і аператыя адката ніколі не будзе выканана:
// api.js
export async function updateLikeOnServer(postId) {
const response = await fetch(`/api/posts/${postId}/like`, {
method: "POST",
});
if (!response.ok) {
throw new Error("Like request failed");
}
return response.json();
}
Пасля трэцьяго крока існуе толькі два можлівыя рэзультаты. Альбо запыт быў успешны і оптымістычны статус стаў правдзивым, альбо ён зазнаў няудачы і інтэрфейс вернуўся да стану з першага крока. У обох случаях інтэрфейс ніколи не пакажае таго, чаго бэкенд не можа падтрымаць прыбліжна на той час, як запыт зазнае няудачы.
Дзе оптымістычныя апдэйты ўзнагароджваюцца
Дадатковая структура ўзнагароджваецца толькі тады, калі аператыя адката є дашчавай, а спрабоўваецца значна падвысіць швальнасць. Тры катэгорыі стаўляюцца пад гэты профіль адносна стабільна:
- Сацыяльныя адносіні. Лайкі, стэпанаванне за адным чалавекам і рэакціі не маюць вялікаг значэння. Якщо адна з багацькох спроб на дзень не успівае, паследкам ёсць тое, што корыстнік прабуе знова.
- Перадача паведамленняў. У дзяловых дапытках, такіх як WhatsApp чыста Slack, паведамленне, апрацоўанае за слабкага з’яёднання, апылваецца ў размове негаразду, часта затуманенае або з маленькім сірым айконам гадзінака. это оптымістычны стан, калі ёсць відразу видна азначка прыготавання: паведамленне з’яўляецца на экране раней, чым досягне сервера.
- Панелі з можласцю ператасоввання. Якбы карточка, яку ператасовваюць у столбець „Завершана“ на панелі канбан, такой як Jira чыста Trello, мусіла чакаць на пאўтварэнне, це была б супрацоўка, падобная ператасовванню через мокры пясак. У оптымістычным варыянте карточка адразу падае на свое месца, а якшо запрос не успівае, ёна вяртаецца у свой пачатковы столбець.
Корыстным удосконаленнем практыкі перадачы поведамленняў є індыкатор чакання. Адзначэнне таго, што дзеянне яшчэ не падтверджанае, дазволяе быць шырокамысловым і чыстасловным адночасна, што зменшае неспакой у разы, калі выканана вярнэнне змян.
Дзе оптымізм стае небяпечным
Тепер уявіцеся аплікацыю для банкавугляду. Жылець нажымае „Адправіць“, каб пераказаць п’яць тысяч долераў як арендную плату власніку. За дапамогою оптымістычнага апошньага апдэйта на экране негайна паведамляецца, што пераказ успешны і залишок тепер стаў нулям, і корыстнік закрывае аплікацыю, думаючы, што арендная плата заплацена.
У закулісся запит не выконваецца; некажаўна, чыраць гэта з-за недастатку скарбоў чы адхіленага пераказу. Пользователь продовжвае верыць, што грошы былі пераказаны, пакуль власнік кварцеры не затрыме і не запытае пра арендную плату. Як мінімум, гэта значыць неабходнасць стварэння заявкі на падтрымку і парадуранне довер'я, а гэта трапляецца таму, што патэрн, створаны для дешавых, вярніх дзеянняў, быў застосаваны там, дзе помылка мае рэальныя наследкі.
Тая ж аберагальнасць паслужыць і для калякоў спадзяючыхся катэгарыяў:
- Фінансавыя транзакцыі. Паказваць індыкатор загрузкі. Чаканне сигналізуе, што система фактычна пераканалася ў правільнасці дзеяння пры яго параджанні.
- Незворачныя выдалення. Калі хтось выдаляе свой акаунт, экран не должен стаць порожнім, пакуль сервер не парадзіцься, што даныя зниклі.
Разлік не ў тым, насколькі значным здаецца паведзенне, а ў тым, якія збиткі можу выйсці ад неправильнага рашэння. Кращэ заставіць чалавека чакаць ўсьмо тры секунды, чым сказаць яму, што грошы пераказаны, калі цього не адбылася.
Калі два натискання змагаюцца межы
Існуе ўзгарожанне, якое з’яўляецца толькі тады, калі корыстнік зноў выкармчуецца раней, чым завершыцца першы запит. На медленным паўзе корыстнік лайкае паст, пераводзячы ўражэнне з 999 на 1 000, а потым негайна скасоўвае лайк, вяртаючы значэнне на 999. Падазроўваючы, што лайкаванне запрацюе за пяць секунд, а скасаванне — толькі за две, скасаванне можа прыйсці першым.
Сервер пасля таго обрабатвае запытку «не спадзялася», калі паведамлення яшчэ не было прызначанае як спадзялася, таму нічога не выходзіць, а за калькі секунд ён обрабатвае запытку «спадзялася». На сервере акаунт становіцца спадзялым, тады калі на экране паказваецца працоўнае становішча, адваротнае да таго, што корыстнік зробіў пасляльш. Калі канечны стан вялічыцца на аснове порядку прыбыць запытак, а не на аснове порядку дзеянняў корыстніка, тады вы маеце условія гонкі.
Анулюванне застарэлых запытак з AbortController
Звычны спосаб — анулюванне: калі пачынаецца новейшая запытка да таго ж ресурса, трэба сказаць браузеру абанаваць старэю. Вбудованный AbortController можа анулюваць fetch, які вэсна ў выпалоўцы. У прыкладзе зберагаецца кансэльтация да актыўнага контролера, які анулюецца перад пачатком новай запытки, а таксама выбіраецца POST чы DELETE на аснове бажанага стану:
// likeButton.js
let activeRequest = null;
async function toggleLike(postId, shouldLike) {
if (activeRequest) {
activeRequest.abort();
}
const controller = new AbortController();
activeRequest = controller;
await fetch(`/api/posts/${postId}/like`, {
method: shouldLike ? "POST" : "DELETE",
signal: controller.signal,
});
}
Кожны новы ўдар па экране анулюеяць паказаны раней запит, таму должна быць выконана толькі пасляпэўнейшая дзеянне. Є два моменты, якія варта знаць пры тым, калі плануецца выкарыстоўваць гэты метод самастоятельна:
- Анулюванне запіта не дазволяе браузеру чакаць на адпаведзенне; гэта не гарантуе, што сервер ніколі не прымеў і не обробіў запит. Ёсць больш надзеяных способаў — трэба спроекаваць канец-тупінь так, каб ён відпаваў бажанаму фінальнаму стану (спадабалося чы ні), тады паўтарныя або пераканфігураваныя запіты будуць адпаваляць адносна таго ж стану, але можна таксама дадаць номер парады, які сервер зможа выкарыстоўваць, каб ігнораваць застарэлыя змены.
- Анульованы запіт
fetchвяртае адмову з кодамAbortError. Якщо вы соўместна выкарыстоўваеце гэта з процедурой анулювання з трэцьага крока, трэба пераканацца ў наявнасці гэтай адмовы і праігнораваць процедуру анулювання, інакш анулюванне застарэлага запіту неправильна скасуе новейшы, оптымістычны стан.
У застосунках на React бібліятэкі дадзеных керуюць большайша частка такіх процэсаў. TanStack Query, які керуе кэшаваннем стану сервера і жыцёвым циклам запитоў, а таксама Redux Toolkit — офіцыйны набор засобаў для напісання логіки Redux — абодва з’яўляюць вбудованую падтрымку для оптымістычных апдэйтаў і анулювання, таму вы рэдка пішаце код AbortController вручную. Пераканайцеся ў іх дасягледах, калі самэ анулюецца ў кожным случае; яшчэ цінная є панорама механізма, які стоіць за гэтым. Чытайце пра тое, як можу паспэўніцца анулюванне з викорыстанням сэрбера React, у стацыі пяць спосабоў парадкі useOptimistic rollbacks.
Ключовыя выводы
- Працэс перадачы даных па сеціі можа быць настолькі повольны, што працюючы застосунак можа выглядаць несправным; таму оптымістычные апдэйты спачатку паказваюць прыемлівы рэзультат, а пазней яго паўтарна падтверджуюць.
fetch не адхоўляе запыт у разе HTTP-памылак, таму трэба выклікаць адхоўленне, калі !response.ok, і паведаміць корыстніка паўтарным спавешчэнням, калі аброшаюцца змяны.AbortError у логіцы аброшэння змян, і вядома выбіраючы канцэнтры, якія задаюць фінальны стан, а не тыя, якія яго змінююць.Скорасць — гэта функцыя, яку сеть не дастане вам безкоштовна. Оптымістичны інтерфейс прыймае тое, што не можа кантролюваць з’ѐеднанне, але ўсё рава впэўнена адпавядае корыстніку і застаецца гатовым вярнуць тую адпаведзь, як толькі це пакажаецца некоректным.